docs: refresh stale documentation (#191)

* docs: sync runtime docs with repo behavior
* docs: refresh stale documentation
* docs: clarify kanban execution and gateway pairing
This commit is contained in:
Çağın Dönmez 2026-03-30 15:53:50 +03:00 committed by GitHub
parent b205b7f654
commit 808a5fcc8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 368 additions and 442 deletions

View file

@ -45,61 +45,78 @@ Thanks for wanting to help! This guide covers everything you need to start contr
# Terminal 1 — Vite frontend with HMR
npm run dev
# Terminal 2 — Backend with file watching
npm run dev:server
# Terminal 2 — Backend with file watching on a separate port
PORT=3081 npm run dev:server
```
5. Open **http://localhost:3080**. The frontend proxies API requests to the backend on `:3081`.
5. Open **http://localhost:3080**. In this split setup, Vite proxies API and WebSocket traffic to the backend on `:3081`.
`npm run dev:server` does not default to `:3081` on its own. Without `PORT=3081`, the backend uses its normal default of `:3080`.
## Project Structure
```
nerve/
openclaw-nerve/
├── src/ # Frontend (React + TypeScript)
│ ├── features/ # Feature modules (co-located)
│ │ ├── auth/ # Login page, auth gate, session hook
│ │ ├── chat/ # Chat panel, messages, input, search
│ │ ├── voice/ # Push-to-talk, wake word, audio feedback
│ │ ├── tts/ # Text-to-speech playback
│ │ ├── sessions/ # Session list, tree, spawn dialog
│ │ ├── workspace/ # Tabbed panel: memory, crons, skills, config
│ │ ├── file-browser/ # Workspace file browser with tabbed editor
│ │ ├── settings/ # Settings drawer (appearance, audio, connection)
│ ├── features/ # Product surfaces and feature-local helpers
│ │ ├── activity/ # Agent log and event log panels
│ │ ├── auth/ # Login gate and auth flows
│ │ ├── charts/ # Inline chart extraction and renderers
│ │ ├── chat/ # Chat UI, message loading, streaming operations
│ │ ├── command-palette/ # ⌘K command palette
│ │ ├── markdown/ # Markdown renderer, code block actions
│ │ ├── charts/ # Inline chart extraction and rendering
│ │ ├── memory/ # Memory editor, add/delete dialogs
│ │ ├── activity/ # Agent log, event log
│ │ ├── dashboard/ # Token usage, memory list, limits
│ │ └── connect/ # Connect dialog (gateway setup)
│ ├── components/ # Shared UI components
│ │ ├── ui/ # Primitives (button, input, dialog, etc.)
│ │ └── skeletons/ # Loading skeletons
│ ├── contexts/ # React contexts (Chat, Session, Gateway, Settings)
│ ├── hooks/ # Shared hooks (WebSocket, SSE, keyboard, etc.)
│ ├── lib/ # Utilities (formatting, themes, sanitize, etc.)
│ ├── types.ts # Shared type definitions
│ └── test/ # Test setup
│ │ ├── connect/ # Gateway connect dialog
│ │ ├── dashboard/ # Token usage and memory list views
│ │ ├── file-browser/ # Workspace tree, tabs, editors
│ │ ├── kanban/ # Task board, proposals, execution views
│ │ ├── markdown/ # Markdown and tool output rendering
│ │ ├── memory/ # Memory editing dialogs and hooks
│ │ ├── sessions/ # Session list, tree helpers, spawn flows
│ │ ├── settings/ # Settings drawer and audio controls
│ │ ├── tts/ # Text-to-speech playback/config
│ │ ├── voice/ # Push-to-talk, wake word, audio feedback
│ │ └── workspace/ # Workspace-scoped panels and state
│ ├── components/ # Shared UI building blocks
│ ├── contexts/ # Gateway, session, chat, and settings contexts
│ ├── hooks/ # Cross-cutting hooks used across features
│ ├── lib/ # Shared frontend utilities
│ ├── App.tsx # Main layout and panel composition
│ └── main.tsx # Frontend entry point
├── server/ # Backend (Hono + TypeScript)
│ ├── routes/ # API route handlers
│ ├── services/ # TTS engines, Whisper, usage tracking
│ ├── lib/ # Utilities (config, WS proxy, file watcher, etc.)
│ ├── middleware/ # Auth, rate limiting, security headers, caching
│ └── app.ts # Hono app assembly
├── config/ # TypeScript configs for server build
│ ├── routes/ # API routes, mounted from server/app.ts
│ │ ├── auth.ts
│ │ ├── gateway.ts
│ │ ├── sessions.ts
│ │ ├── workspace.ts
│ │ ├── files.ts
│ │ ├── file-browser.ts
│ │ ├── kanban.ts
│ │ ├── crons.ts
│ │ ├── memories.ts
│ │ ├── tts.ts
│ │ ├── transcribe.ts
│ │ └── ...plus route tests beside many handlers
│ ├── services/ # Whisper, TTS, and related backend services
│ ├── lib/ # Config, gateway helpers, cache, file watchers, mutexes
│ ├── middleware/ # Auth, security headers, cache, limits
│ ├── app.ts # Hono app assembly
│ └── index.ts # HTTP/HTTPS server startup
├── bin/ # CLI/update entrypoints
├── config/ # TypeScript build configs
├── docs/ # User and operator docs
├── public/ # Static assets
├── scripts/ # Setup wizard and utilities
├── docs/ # Documentation
├── vitest.config.ts # Test configuration
├── eslint.config.js # Lint configuration
└── vite.config.ts # Vite build configuration
├── vite.config.ts # Vite config
├── vitest.config.ts # Vitest config
└── eslint.config.js # ESLint flat config
```
### Key conventions
- **Feature modules** live in `src/features/<name>/`. Each feature owns its components, hooks, types, and tests.
- **`@/` import alias** maps to `src/` — use it for cross-feature imports.
- **Tests are co-located** with source files: `foo.ts``foo.test.ts`.
- **Server routes** are thin handlers that delegate to `services/` and `lib/`.
- **Feature modules** usually live in `src/features/<name>/`. Keep new UI work inside the closest existing feature instead of inventing a parallel structure.
- **`@/` import alias** maps to `src/`.
- **Tests are usually nearby** the code they cover, especially for hooks, routes, and utilities.
- **Cross-feature imports exist**, but keep them narrow and intentional. Reuse small helpers, avoid circular dependencies, and do not spread one-off shortcuts across the app.
- **Server routes** live in `server/routes/` and are mounted in `server/app.ts`. Shared logic belongs in `server/lib/`, `server/services/`, or `server/middleware/`.
## Adding a Feature

View file

@ -136,10 +136,12 @@ Fetches the latest release, rebuilds, restarts, verifies health, and rolls back
<details><summary><strong>Development</strong></summary>
```bash
npm run dev # frontend — Vite HMR on :3080
npm run dev:server # backend — watch mode on :3081
npm run dev # frontend — Vite on :3080 by default
PORT=3081 npm run dev:server # backend — explicit split-port dev setup
```
`npm run dev:server` uses the normal server `PORT` setting. If you do not override it, the backend also defaults to `:3080` and will collide with Vite.
**Requires:** Node.js 22+ and an OpenClaw gateway.
</details>

View file

@ -131,9 +131,9 @@ The marker must contain valid JSON inside `[chart:{...}]`. The parser uses brack
### How Agents Learn About Charts
Unlike TTS markers (which use runtime prompt injection), chart markers are taught to agents via the **`TOOLS.md` workspace file**. Nerve's installer can inject chart documentation into `TOOLS.md` automatically (see PR #218).
Unlike TTS markers, chart markers are **not** injected by Nerve at runtime.
Agents that have the chart syntax in their `TOOLS.md` will naturally include `[chart:{...}]` markers when data visualization is appropriate.
Agents only use `[chart:{...}]` markers when that syntax is already present in their own instructions or workspace context, for example in `TOOLS.md`, `AGENTS.md`, or another prompt source you manage.
### Implementation

View file

@ -21,7 +21,7 @@ Nerve exposes a REST + SSE API served by [Hono](https://hono.dev/) on the config
- [Memories](#memories)
- [Agent Log](#agent-log)
- [Gateway](#gateway)
- [Git Info](#git-info)
- [Sessions](#sessions)
- [Workspace Files](#workspace-files)
- [Cron Jobs](#cron-jobs)
- [Skills](#skills)
@ -427,8 +427,8 @@ Transcribes audio using the configured STT provider.
| Model | Size | Speed | Quality |
|-------|------|-------|---------|
| `tiny` (default) | 75 MB | Fastest | Good baseline, multilingual |
| `base` | 142 MB | Fast | Better conversational accuracy, multilingual |
| `tiny` | 75 MB | Fastest | Good baseline, multilingual |
| `base` (default) | 142 MB | Fast | Better conversational accuracy, multilingual |
| `small` | 466 MB | Moderate | Best accuracy (CPU-intensive), multilingual |
| `tiny.en` | 75 MB | Fastest | English-only variant |
| `base.en` | 142 MB | Fast | English-only variant |
@ -471,7 +471,7 @@ Returns current STT runtime config + local model readiness/download state.
```json
{
"provider": "local",
"model": "tiny",
"model": "base",
"language": "en",
"modelReady": true,
"openaiKeySet": false,
@ -479,7 +479,7 @@ Returns current STT runtime config + local model readiness/download state.
"hasGpu": false,
"availableModels": {
"tiny": { "size": "75MB", "ready": true, "multilingual": true },
"base": { "size": "142MB", "ready": false, "multilingual": true },
"base": { "size": "142MB", "ready": true, "multilingual": true },
"tiny.en": { "size": "75MB", "ready": true, "multilingual": false }
},
"download": null
@ -580,7 +580,7 @@ Returns full provider × language support matrix and current local model state.
"tts": { "edge": true, "qwen3": true, "openai": true }
}
],
"currentModel": "tiny",
"currentModel": "base",
"isMultilingual": true
}
```
@ -867,7 +867,7 @@ All fields are optional. A `ts` (epoch ms) is automatically set on write. The lo
### `GET /api/gateway/models`
Returns available AI models from the OpenClaw gateway. Models are fetched via `openclaw models list`, cached for 5 minutes, and the CLI call allows a longer timeout so cold starts have more time to surface configured models in the spawn dialog.
Returns the models defined in the active OpenClaw config. This endpoint is config-backed now, not CLI-discovered or cache-backed.
**Rate Limit:** General (60/min)
@ -876,17 +876,26 @@ Returns available AI models from the OpenClaw gateway. Models are fetched via `o
```json
{
"models": [
{ "id": "anthropic/claude-sonnet-4-20250514", "label": "claude-sonnet-4-20250514", "provider": "anthropic" },
{ "id": "openai/gpt-4o", "label": "gpt-4o", "provider": "openai" }
]
{
"id": "anthropic/claude-sonnet-4-20250514",
"label": "claude-sonnet-4-20250514",
"provider": "anthropic",
"configured": true,
"role": "primary"
}
],
"error": null,
"source": "config"
}
```
**Selection logic:**
1. Configured / allowlisted models (from `agents.defaults.models` in OpenClaw config) are fetched first and included regardless of `available` flag
2. If that returns 0 models, Nerve falls back to `openclaw models list --all --json` and keeps only available entries
| Field | Type | Description |
|-------|------|-------------|
| `models` | `array` | Configured models from `agents.defaults.model` and `agents.defaults.models` in the active OpenClaw config |
| `error` | `string \| null` | Read error or configuration problem, for example config unreadable or no configured models |
| `source` | `"config"` | Identifies the backing source |
The CLI timeout for model discovery is **15 seconds**, which helps cold or recently started OpenClaw installs surface configured models more reliably.
Model roles are `primary`, `fallback`, or `allowed`. If the config cannot be read, or no models are configured, the endpoint returns an empty `models` array with an explanatory `error` string.
### `GET /api/gateway/session-info`
@ -911,7 +920,7 @@ Resolution order: per-session data from `sessions_list` → global `gateway_stat
### `POST /api/gateway/session-patch`
Changes the model and/or thinking level for a session. HTTP fallback when WebSocket RPC fails.
HTTP fallback for **model changes** when the frontend cannot apply `sessions.patch` over WebSocket. Thinking-only changes are not supported here.
**Rate Limit:** General (60/min)
@ -925,61 +934,111 @@ Changes the model and/or thinking level for a session. HTTP fallback when WebSoc
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `sessionKey` | `string` | No | Target session. If omitted, Nerve tries to pick a preferred active root session |
| `model` | `string` | No | Model to apply via the gateway's `session_status` tool |
| `thinkingLevel` | `string \| null` | No | Accepted by the schema, but not applied by this HTTP fallback |
**Response:**
```json
{ "ok": true, "model": "anthropic/claude-sonnet-4-20250514", "thinking": "high" }
{ "ok": true, "model": "anthropic/claude-sonnet-4-20250514" }
```
**Errors:** 400 (invalid JSON), 502 (gateway tool invocation failed)
**Behavior notes:**
- Thinking changes belong on WebSocket RPC `sessions.patch`, alongside other session metadata/settings updates.
- A request that only changes `thinkingLevel` returns **501**.
- If no active root session can be found and `sessionKey` is omitted, the endpoint returns **409**.
- If both `model` and `thinkingLevel` are sent, the model change is applied and the thinking change is ignored.
**Errors:**
| Status | Condition |
|--------|-----------|
| 400 | Invalid JSON or validation error |
| 409 | No active root session available |
| 501 | Thinking-only changes are not supported over HTTP |
| 502 | Model change failed |
### `POST /api/gateway/restart`
Restarts the OpenClaw gateway service, then waits for the service to report healthy status and for the gateway port to become reachable.
**Rate Limit:** Restart (3/min)
**Response:**
```json
{ "ok": true, "output": "Gateway restarted successfully" }
```
**Errors:** 500 if restart or post-restart verification fails.
---
## Git Info
## Sessions
### `GET /api/git-info`
### `GET /api/sessions/hidden`
Returns the current git branch and dirty status.
Returns hidden cron-like sessions from `sessions.json`, sorted by recent activity. Used to surface session metadata that is not part of the normal active session tree.
**Rate Limit:** General (60/min)
**Query Parameters:**
| Param | Description |
|-------|-------------|
| `sessionKey` | Use a registered session-specific working directory |
| Param | Default | Description |
|-------|---------|-------------|
| `activeMinutes` | `1440` | Include sessions updated within the last N minutes |
| `limit` | `200` | Maximum results. Clamped to `2000` |
**Response:**
```json
{ "branch": "main", "dirty": true }
{
"ok": true,
"sessions": [
{
"key": "agent:main:cron:daily:run:abc",
"sessionKey": "agent:main:cron:daily:run:abc",
"id": "123e4567-e89b-12d3-a456-426614174000",
"label": "daily summary",
"displayName": "daily summary",
"updatedAt": 1708100000000,
"model": "openai/gpt-5",
"thinking": "medium",
"thinkingLevel": "medium",
"totalTokens": 1234,
"contextTokens": 456,
"parentId": "agent:main:cron:daily"
}
]
}
```
Returns `{ "branch": null, "dirty": false }` if not in a git repo.
If the backing `sessions.json` file is unavailable, the endpoint returns `{ "ok": true, "sessions": [] }`. In remote-workspace cases it may also include `remoteWorkspace: true`.
### `POST /api/git-info/workdir`
### `GET /api/sessions/:id/model`
Registers a working directory for a session, so `GET /api/git-info?sessionKey=...` resolves to the correct repo.
Reads the actual model used by a session from its transcript. This is mainly for cron-run sessions where gateway session listings may only expose the parent agent's default model.
**Request Body:**
**Rate Limit:** General (60/min)
**Path Parameters:**
| Param | Description |
|-------|-------------|
| `id` | Session UUID |
**Response:**
```json
{ "sessionKey": "agent:main:subagent:abc123", "workdir": "/home/user/project" }
{ "ok": true, "model": "openai/gpt-5", "missing": false }
```
The workdir must be within the allowed base directory (derived from `WORKSPACE_ROOT` env var, git worktree list, or the parent of `process.cwd()`). Returns 403 if the path is outside the allowed base.
If the transcript cannot be found, the endpoint returns `{ "ok": true, "model": null, "missing": true }`.
Session workdir entries expire after 1 hour. Max 100 entries.
### `DELETE /api/git-info/workdir`
Unregisters a session's working directory.
**Request Body:**
```json
{ "sessionKey": "agent:main:subagent:abc123" }
```
**Errors:** 400 if `id` is not a valid UUID.
---
@ -1657,7 +1716,7 @@ Move a task to a different position within its column or to another column. CAS-
### `POST /api/kanban/tasks/:id/execute`
Execute a task by spawning an agent session. The task must be in `todo` or `backlog` status. Moves the task to `in-progress` and starts polling the agent session for completion.
Execute a task and move it to `in-progress`. The launch path depends on the task's assignee and platform. The task must be in `todo` or `backlog` status. Moves the task to `in-progress` and starts polling the agent session for completion.
**Rate Limit:** General (60/min)
@ -1672,22 +1731,29 @@ Execute a task by spawning an agent session. The task must be in `todo` or `back
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `model` | `string` | No | Model override (max 200 chars). Falls back to task's model → board `defaultModel``anthropic/claude-sonnet-4-5` |
| `thinking` | `string` | No | Thinking level: `off`, `low`, `medium`, `high` |
| `model` | `string` | No | Execution model override (max 200 chars). Cascade: execute request → task `model` → board `defaultModel` → OpenClaw configured default |
| `thinking` | `string` | No | Thinking override: `off`, `low`, `medium`, `high` |
**Response:** The updated `KanbanTask` object with `status: "in-progress"` and a `run` object.
**Execution paths:**
- **Assigned tasks** run through the assignee's live root session. Nerve verifies that parent root exists, then asks it to spawn a child worker.
- **Unassigned or `operator` tasks** use the normal `sessions_spawn` path.
- **macOS fallback rule:** unassigned or `operator` tasks are rejected. Assign the task to a live worker root first.
**Errors:**
| Status | Body | Condition |
|--------|------|-----------|
| 404 | `{ "error": "not_found" }` | Task not found |
| 409 | `{ "error": "duplicate_execution" }` | Task is already running |
| 409 | `{ "error": "invalid_execution_target" }` | Required parent root is missing, or macOS requires an assigned live worker root |
| 409 | `{ "error": "invalid_transition", "from": "done", "to": "in-progress" }` | Task not in `todo` or `backlog` status |
**Notes:**
- If the task is already `in-progress` with an active run, returns the task as-is (idempotent).
- The spawned agent receives the task title and description as its prompt.
- The backend polls the gateway every 5 seconds for up to 30 minutes. On completion, the task moves to `review`. On error, it moves back to `todo`.
- The spawned worker receives the task title and description as its prompt.
- Backend pollers run every 5 seconds for up to **720 attempts / 60 minutes**.
- On success the task moves to `review`. On error it moves back to `todo`.
### `POST /api/kanban/tasks/:id/complete`
@ -1695,10 +1761,11 @@ Complete a running task. Called by the backend poller automatically, but can als
**Rate Limit:** General (60/min)
**Request Body (optional):**
**Request Body:**
```json
{
"sessionKey": "kb-auth-refactor-123-v4-1708100000000",
"result": "Refactored auth module. Extracted SessionService class...",
"error": "Agent session timed out"
}
@ -1706,6 +1773,7 @@ Complete a running task. Called by the backend poller automatically, but can als
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `sessionKey` | `string` | Yes | Active run session key used to match the task run |
| `result` | `string` | No | Agent output text (max 50000 chars). Kanban markers are parsed and stripped automatically |
| `error` | `string` | No | Error message (max 5000 chars). If set, task moves to `todo` instead of `review` |
@ -1715,8 +1783,9 @@ Complete a running task. Called by the backend poller automatically, but can als
| Status | Condition |
|--------|-----------|
| 400 | Invalid body or missing `sessionKey` |
| 404 | Task not found |
| 409 | No active run to complete |
| 409 | No active matching run to complete |
### `POST /api/kanban/tasks/:id/approve`
@ -2008,7 +2077,8 @@ All `/api/*` routes have rate limiting applied. Limits are per-client-IP per-pat
|--------|--------|-------|
| **TTS** | `POST /api/tts` | 10 requests / 60 seconds |
| **Transcribe** | `POST /api/transcribe` | 30 requests / 60 seconds |
| **General** | All other `/api/*` routes | 60 requests / 60 seconds |
| **General** | Most `/api/*` routes | 60 requests / 60 seconds |
| **Restart** | `POST /api/gateway/restart` | 3 requests / 60 seconds |
**Rate limit headers** are included on every response:

View file

@ -346,12 +346,16 @@ Applied in order in `app.ts`:
| `/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 | Available models via `openclaw models list`, with longer cold-start timeout, allowlist support, and `--all` fallback |
| `/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 | Change model/effort for a session |
| `/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/git-info` | `routes/git-info.ts` | GET, POST, DELETE | Git branch/status. Session workdir registration |
| `/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 |
@ -359,6 +363,7 @@ Applied in order in `app.ts`:
| `/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 |
@ -486,7 +491,7 @@ The frontend calls gateway methods via `GatewayContext.rpc()`:
| `sessions.list` | List active sessions |
| `sessions.delete` | Delete a session |
| `sessions.reset` | Clear session context |
| `sessions.patch` | Rename a session |
| `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 |
@ -565,26 +570,29 @@ This prevents stale overwrites from concurrent editors (drag-and-drop, API clien
```
1. POST /api/kanban/tasks/:id/execute
+-- withMutex(`kanban-execute:${id}`) prevents double-launch races
+-- store.executeTask(..., { sessionKey }) -> status = in-progress, run.status = running
+-- Primary path (Linux / existing master behavior):
| +-- invokeGatewayTool('sessions_spawn', { task, mode:'run', label: runSessionKey, model?, thinking? })
| +-- attach childSessionKey / runId when available
| +-- start pollSessionCompletion(taskId, { correlationKey: runSessionKey, childSessionKey?, runId? })
|
+-- macOS fallback path (intentional platform compromise):
+-- if task already in-progress: return 409 duplicate_execution
+-- if task has an assignee root:
| +-- resolve assignee root -> agent:<assignee>:main
| +-- gatewayRpcCall('sessions.list', ...) to confirm the parent root exists
| +-- gatewayRpcCall('sessions.list', ...) confirms the parent root exists
| +-- store.executeTask(..., { sessionKey }) -> status = in-progress, run.status = running
| +-- launchKanbanFallbackSubagentViaRpc({ label, task, parentSessionKey, model?, thinking? })
| +-- gatewayRpcCall('chat.send', { sessionKey: parentSessionKey, message:'[spawn-subagent]...', idempotencyKey })
| +-- attach returned runId when available
| +-- start pollFallbackSessionCompletion(taskId, { correlationKey, parentSessionKey, 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()
+-- primary path polls gateway subagents for the run correlation key / runId
+-- macOS fallback polls direct gateway RPC sessions.list every 5s (max 720 attempts / 60 min)
+-- macOS fallback matches the spawned child beneath the assignee root and attaches childSessionKey
+-- 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 matches the spawned child beneath the parent root and attaches childSessionKey
+-- both paths complete the task when the child reports terminal success/failure
+-- if session not found yet:
+-- schedule next poll
@ -605,7 +613,7 @@ This prevents stale overwrites from concurrent editors (drag-and-drop, API clien
+-- error -> run.status = error, task.status = todo
```
The model cascade is: task `model` -> execute request `model` -> board config `defaultModel` -> OpenClaw's configured default model. Thinking follows the same cascade with `defaultThinking`.
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
@ -639,7 +647,7 @@ The `proposalPolicy` config controls behavior:
| 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 360 attempts = 30 minutes). Stale runs are reconciled by `reconcileStaleRuns()`.
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()`.
---

View file

@ -1,302 +1,113 @@
# Code Review Guide
Standards, patterns, and review checklist for the Nerve codebase.
Review Nerve as it exists today, not as an imaginary perfect architecture.
---
## First principle
## Coding Standards
Prefer consistency with the surrounding subsystem over introducing a brand-new pattern just because it looks cleaner in isolation. Nerve has strong structure, but it is still a living codebase with some mixed styles and a few rough edges. Good review reduces drift. It does not create more of it.
## Repo reality, right now
### TypeScript
- **Strict mode** enabled across all tsconfig project references
- **Explicit types** on all public interfaces, context values, and hook returns
- **Discriminated unions** for message types (`GatewayEvent | GatewayRequest | GatewayResponse` via `type` field)
- **Typed event payloads**`AgentEventPayload`, `ChatEventPayload`, `CronEventPayload` instead of `any`
- **Zod validation** on all API request bodies (server-side)
- **No `any`** — use `unknown` with type narrowing
- The repo runs TypeScript in strict mode.
- Most application code is strongly typed.
- A few internal utilities and tests still use `any`. Do not spread that pattern. If you can tighten a touched area safely, do it.
- `@ts-ignore` should be rare and justified inline.
### React
### Frontend structure
- **Functional components only** — no class components
- **`useCallback` / `useMemo`** on all callbacks and derived values passed to children or used in dependency arrays
- **`React.memo`** is not used broadly; instead, stable references via `useMemo`/`useCallback` prevent unnecessary re-renders
- **Ref-based state access** in callbacks that shouldn't trigger re-registration (e.g., `currentSessionRef`, `isGeneratingRef`, `soundEnabledRef`)
- **ESLint annotations** when intentionally breaking rules: `// eslint-disable-next-line react-hooks/set-state-in-effect -- valid: <reason>`
- The frontend is organized mostly by feature under `src/features/`.
- Shared layers live in `src/components/`, `src/contexts/`, `src/hooks/`, and `src/lib/`.
- Large UI surfaces are often lazy-loaded, especially settings, sessions, workspace, kanban, charts, and file editing.
- Cross-feature imports do exist. Keep them narrow, stable, and free of circular dependencies.
### Naming
### React patterns worth preserving
- **Files:** PascalCase for components (`ChatPanel.tsx`), camelCase for hooks/utils (`useWebSocket.ts`, `helpers.ts`)
- **Contexts:** `<Name>Context` with `<Name>Provider` and `use<Name>` hook co-located in same file
- **Feature directories:** kebab-case (`command-palette/`)
- **Types:** PascalCase interfaces/types, `I` prefix NOT used
- Functional components and hooks only.
- Stable callbacks and memoized derived values matter in hot paths like chat, sessions, file-browser, workspace switching, and kanban.
- Ref-synchronized state is used in a few places where callbacks need fresh values without constantly re-registering listeners.
- Optional or heavy panels are usually wrapped in `Suspense` and `PanelErrorBoundary`.
---
### Backend structure
## Architectural Patterns
- Backend routes live in `server/routes/` and are mounted from `server/app.ts`.
- Shared behavior lives in `server/lib/`, `server/services/`, and `server/middleware/`.
- Some write endpoints use `zValidator` and Zod today, but not all of them. New write endpoints should validate input. When you touch an older route, tightening validation is a good upgrade if it stays low-risk.
- File and state mutations that can race are often protected with a mutex.
- `/api/events` is an SSE endpoint and must not be buffered or compressed.
### 1. Feature-Based Directory Structure
### Security and config baseline
```
src/features/
chat/
ChatPanel.tsx # Main component
components/ # Sub-components
operations/ # Pure business logic (no React)
types.ts # Feature-specific types
utils.ts # Feature utilities
sessions/
workspace/
settings/
tts/
voice/
...
```
- Auth, origin handling, body limits, and WebSocket allowlists are part of the product surface, not optional polish.
- `HOST=0.0.0.0` without auth is intentionally blocked unless the explicit insecure override is set.
- New env vars should land in `.env.example` and `docs/CONFIGURATION.md` in the same PR.
Each feature is self-contained. Cross-feature imports go through context providers, not direct imports.
## Review priorities
### 2. Context Provider Pattern
1. Correctness and regressions
2. Security and data exposure
3. Consistency with nearby patterns
4. Operability, tests, docs, and maintainability
5. Style polish
Every context follows the same structure:
## Review checklist
```tsx
const MyContext = createContext<MyContextValue | null>(null);
### General
export function MyProvider({ children }: { children: ReactNode }) {
// State, effects, callbacks
const value = useMemo<MyContextValue>(() => ({
// All exposed values
}), [/* dependencies */]);
return <MyContext.Provider value={value}>{children}</MyContext.Provider>;
}
- [ ] The behavior change is intentional, and the PR description matches the diff.
- [ ] Commands, ports, env vars, and docs match the current repo behavior.
- [ ] New files live in the right area instead of creating a parallel structure.
- [ ] Naming follows nearby code more than abstract preference.
- [ ] Dead branches, debug noise, and commented-out code are not slipping in.
export function useMyContext() {
const ctx = useContext(MyContext);
if (!ctx) throw new Error('useMyContext must be used within MyProvider');
return ctx;
}
```
### Frontend
Key characteristics:
- Context value is always `useMemo`-wrapped with explicit type annotation
- `null` default with runtime check in the hook
- Provider, context, and hook co-located in one file (ESLint `react-refresh/only-export-components` disabled with reason)
- [ ] Changes fit the current feature, context, and hook split used in that part of the app.
- [ ] Chat, sessions, file-browser, workspace, and kanban changes avoid obvious rerender or subscription churn.
- [ ] Timers, listeners, sockets, observers, and intervals clean up correctly.
- [ ] Heavy or optional UI stays lazy-loaded unless there is a clear reason to change that.
- [ ] Error states, loading states, and mobile behavior still make sense.
- [ ] Keyboard navigation and focus behavior are preserved for dialogs, drawers, and menus.
### 3. Ref-Synchronized State
### Backend
For callbacks that need current state but shouldn't re-register:
- [ ] New route files are mounted in `server/app.ts`.
- [ ] Auth, CORS, body limits, and rate limiting are preserved or improved.
- [ ] Request bodies are validated or parsed narrowly, especially on write endpoints.
- [ ] Shared gateway helpers are reused instead of duplicating request logic.
- [ ] File writes remain atomic where concurrent access is possible.
- [ ] SSE and WebSocket behavior are not broken by buffering, compression, or auth changes.
```tsx
const currentSessionRef = useRef(currentSession);
useEffect(() => {
currentSessionRef.current = currentSession;
}, [currentSession]);
### Tests
// In callbacks: use currentSessionRef.current instead of currentSession
const handleSend = useCallback(async (text: string) => {
await sendChatMessage({ sessionKey: currentSessionRef.current, ... });
}, [rpc]); // Note: currentSession NOT in deps
```
- [ ] New parsing, state, routing, or persistence logic has tests where the repo already tests similar code.
- [ ] Existing tests were updated when behavior changed.
- [ ] Assertions were not weakened just to get green.
This pattern is used extensively in `ChatContext`, `SessionContext`, and `GatewayContext`.
### Docs and operations
### 4. Lazy Loading
- [ ] User-facing changes update README or docs when needed.
- [ ] New config or migration work updates `.env.example`, setup docs, and upgrade notes.
- [ ] Deployment, updater, or gateway-integration changes keep the docs honest.
Heavy components are code-split via `React.lazy`:
## High-signal review comments
```tsx
const SettingsDrawer = lazy(() => import('@/features/settings/SettingsDrawer')
.then(m => ({ default: m.SettingsDrawer })));
const CommandPalette = lazy(() => import('@/features/command-palette/CommandPalette')
.then(m => ({ default: m.CommandPalette })));
const SessionList = lazy(() => import('@/features/sessions/SessionList')
.then(m => ({ default: m.SessionList })));
const WorkspacePanel = lazy(() => import('@/features/workspace/WorkspacePanel')
.then(m => ({ default: m.WorkspacePanel })));
```
Good review comments in this repo are concrete:
Each wrapped in `<Suspense>` and `<PanelErrorBoundary>` for graceful degradation.
- point to the exact mismatch
- explain the user or operator impact
- suggest the smallest fix that matches local patterns
### 5. Operations Layer (Pure Logic Extraction)
Examples:
`ChatContext` delegates to pure functions in `features/chat/operations/`:
- "This route writes state but skips input validation, while nearby write routes parse JSON explicitly. Can we add a schema or a narrow parser here?"
- "This panel is now imported eagerly, which pulls file editor code into the initial bundle. Was that intentional?"
- "The doc says `npm run dev:server` uses `:3081`, but the script only does that when `PORT=3081` is set."
```
operations/
index.ts # Re-exports all operations
loadHistory.ts # loadChatHistory()
sendMessage.ts # buildUserMessage(), sendChatMessage()
streamEventHandler.ts # classifyStreamEvent(), extractStreamDelta(), etc.
```
## Avoid this
This separates React state management from business logic, making operations testable without rendering.
### 6. Event Fan-Out (Pub/Sub)
`GatewayContext` implements a subscriber pattern:
```tsx
const subscribersRef = useRef<Set<EventHandler>>(new Set());
const subscribe = useCallback((handler: EventHandler) => {
subscribersRef.current.add(handler);
return () => { subscribersRef.current.delete(handler); };
}, []);
// In onEvent:
for (const handler of subscribersRef.current) {
try { handler(msg); } catch (e) { console.error(e); }
}
```
Consumers (`SessionContext`, `ChatContext`) subscribe in `useEffect` and receive all gateway events.
### 7. Smart Session Diffing
`SessionContext.refreshSessions()` preserves object references for unchanged sessions:
```tsx
setSessions(prev => {
const prevMap = new Map(prev.map(s => [getSessionKey(s), s]));
let hasChanges = false;
const merged = newSessions.map(newSession => {
const existing = prevMap.get(key);
if (!existing) { hasChanges = true; return newSession; }
const changed = existing.state !== newSession.state || ...;
if (changed) { hasChanges = true; return newSession; }
return existing; // Preserve reference
});
return hasChanges ? merged : prev;
});
```
### 8. Server Route Pattern (Hono)
Each route file exports a Hono sub-app:
```tsx
const app = new Hono();
app.get('/api/something', rateLimitGeneral, async (c) => { ... });
export default app;
```
Routes are mounted in `app.ts` via `app.route('/', route)`.
### 9. Gateway Tool Invocation
Server routes that need gateway interaction use the shared client:
```tsx
import { invokeGatewayTool } from '../lib/gateway-client.js';
const result = await invokeGatewayTool('cron', { action: 'list' });
```
### 10. Mutex-Protected File I/O
File operations that need atomicity use the mutex:
```tsx
import { createMutex } from '../lib/mutex.js';
const withLock = createMutex();
await withLock(async () => {
const data = await readJSON(file, []);
data.push(entry);
await writeJSON(file, data);
});
```
### 11. Cached Fetch with Deduplication
Expensive operations use `createCachedFetch` which deduplicates in-flight requests:
```tsx
const fetchLimits = createCachedFetch(
() => expensiveApiCall(),
5 * 60 * 1000, // 5 min TTL
{ isValid: (result) => result.available }
);
```
---
## Server-Side Patterns
### Security
- **Authentication:** Session-cookie auth via `middleware/auth.ts`. When enabled, all `/api/*` routes (except auth/health) require a valid HMAC-SHA256 signed cookie. WebSocket upgrades checked in `ws-proxy.ts`
- **Session tokens:** Stateless signed cookies (`HttpOnly`, `SameSite=Strict`). Password hashing via scrypt. Gateway token accepted as fallback password
- **CORS:** Strict origin allowlist — only localhost variants and explicitly configured origins
- **Token exposure:** Managed gateway auth uses server-side token injection. `/api/connect-defaults` returns `token: null` and trust metadata instead of the raw gateway token
- **Device identity:** Ed25519 keypair for gateway WS auth (`~/.nerve/device-identity.json`). Required for operator scopes on OpenClaw 2026.2.19+
- **File serving:** MIME-type allowlist + directory traversal prevention + allowed prefix check
- **Body limits:** Configurable per-route (general API vs transcribe uploads)
- **Rate limiting:** Per-IP sliding window with separate limits for expensive operations
- **Credentials:** Browser connection config persists in `localStorage` as `oc-config`. Official managed gateway flows can keep the token empty; custom manual tokens may persist until cleared
- **Input validation:** Zod schemas on all POST/PUT request bodies
### Graceful Shutdown
`server/index.ts` handles SIGTERM/SIGINT:
1. Stop file watchers
2. Close all WebSocket connections
3. Close HTTP + HTTPS servers
4. Force exit after 5s drain timeout
### Dual HTTP/HTTPS
Server runs on both HTTP (port 3080) and HTTPS (port 3443). HTTPS auto-enables if `certs/cert.pem` + `certs/key.pem` exist. HTTPS is required for:
- Microphone access (secure context)
- WSS proxy (encrypted WebSocket)
The HTTPS server manually converts Node.js `req`/`res` to `fetch` `Request`/`Response` for Hono compatibility, with special handling for SSE streaming.
---
## Review Checklist
### All PRs
- [ ] TypeScript strict — no `any`, no `@ts-ignore`
- [ ] All new API endpoints have rate limiting middleware
- [ ] All POST/PUT bodies validated with Zod
- [ ] New state in contexts is `useMemo`/`useCallback`-wrapped
- [ ] No secrets in client-side code or localStorage
- [ ] Error boundaries around lazy-loaded or side-panel components
- [ ] Tests for new utilities/hooks (at minimum)
### Frontend PRs
- [ ] New components follow feature directory structure
- [ ] Heavy components are lazy-loaded if not needed at initial render
- [ ] Callbacks use `useCallback` if passed as props or in dependency arrays
- [ ] State-setting in effects has ESLint annotation with justification
- [ ] No direct cross-feature imports (use contexts)
- [ ] Cleanup functions in `useEffect` for subscriptions/timers/RAF
- [ ] Keyboard shortcuts registered via `useKeyboardShortcuts`
### Backend PRs
- [ ] Routes export a Hono sub-app, mounted in `app.ts`
- [ ] File I/O wrapped in mutex when read-modify-write
- [ ] Gateway calls use `invokeGatewayTool()` from shared client
- [ ] Expensive fetches wrapped in `createCachedFetch`
- [ ] SSE-aware: don't break compression exclusion for `/api/events`
- [ ] CORS: new endpoints automatically covered by global middleware
- [ ] Security: file serving paths validated against allowlist
### Performance
- [ ] No unnecessary re-renders (check with React DevTools Profiler)
- [ ] Session list uses smart diffing (preserves references)
- [ ] Streaming updates use `requestAnimationFrame` batching
- [ ] Large data (history) uses infinite scroll, not full render
- [ ] Activity sparkline and polling respect `document.visibilityState`
### Accessibility
- [ ] Skip-to-content link present (`<a href="#main-chat" class="sr-only">`)
- [ ] Dialogs have proper focus management
- [ ] Keyboard navigation works for all interactive elements
- [ ] Color contrast meets WCAG AA (themes should preserve this)
- Enforcing absolutes the repo does not actually follow
- Requesting wide refactors in a focused bugfix PR
- Rejecting a change for not matching an architecture that is not present in the codebase
- Treating review as style theater while missing correctness or security issues

View file

@ -26,10 +26,10 @@ Connects Nerve to your OpenClaw gateway. The wizard auto-detects the gateway tok
2. Environment variable `OPENCLAW_GATEWAY_TOKEN`
3. `~/.openclaw/openclaw.json` (auto-detected)
Tests the connection before proceeding. If the gateway is unreachable, you can continue anyway. On OpenClaw 2026.2.19+, the wizard also:
Tests the connection before proceeding. If the gateway is unreachable, setup stops so you can fix the gateway or token first. On current OpenClaw builds, the wizard also:
- Reads the real gateway token from the systemd service file (works around a known bug where `openclaw onboard` writes different tokens to systemd and `openclaw.json`)
- Bootstraps `paired.json` and `device-auth.json` with full operator scopes if they don't exist yet
- Pre-registers Nerve's device identity so it can connect without manual `openclaw devices approve`
- Pre-pairs Nerve's device identity in the normal setup path so it can connect without manual approval (`openclaw devices approve`)
- Restarts the gateway to apply changes
#### 2. Agent Identity
@ -48,7 +48,7 @@ Determines how you'll access Nerve. The wizard auto-configures `HOST`, `ALLOWED_
| **Network (LAN)** | `0.0.0.0` | Accessible from your local network. Prompts for your LAN IP. Sets CORS + CSP for that IP. |
| **Custom** | Manual | Full manual control: custom port, bind address, HTTPS certificate generation, CORS. |
**HTTPS (Custom mode only):** The wizard can generate self-signed certificates via `openssl` and configure `SSL_PORT`.
**HTTPS (Network and Custom modes):** The wizard can offer self-signed certificate generation via `openssl` and configure `SSL_PORT` for non-localhost access.
#### 4. Authentication
@ -80,7 +80,7 @@ Custom file paths for `MEMORY_PATH`, `MEMORY_DIR`, `SESSIONS_DIR`. Most users sk
| `--defaults --access-mode tailscale-ip` | Non-interactive setup for direct tailnet IP access. |
| `--defaults --access-mode tailscale-serve` | Non-interactive setup for loopback + Tailscale Serve HTTPS access. |
The wizard backs up existing `.env` files (e.g. `.env.bak.1708100000000`) before overwriting and applies `chmod 600` to both `.env` and backup files.
The wizard backs up existing `.env` files as `.env.backup` or `.env.backup.YYYY-MM-DD` before overwriting and applies `chmod 600` to both `.env` and backup files.
---
@ -126,7 +126,7 @@ curl -fsSL https://raw.githubusercontent.com/daggerhashimoto/openclaw-nerve/mast
Nerve performs **server-side token injection**. When a connection is established through the WebSocket proxy, Nerve automatically injects the configured `GATEWAY_TOKEN` into the connection request if the client is considered **trusted**.
**Trust is granted if:**
1. The connection is from a **local loopback address** (`127.0.0.1` or `::1`), accounting for `X-Forwarded-For` and `X-Real-IP` when behind a trusted proxy (see `TRUSTED_PROXIES`).
1. The connection is from a **local loopback address** (`127.0.0.1` or `::1`). When Nerve is behind a trusted reverse proxy, proxy-aware client IP handling can preserve that loopback detection (see `TRUSTED_PROXIES`).
2. OR, the connection has a valid **authenticated session** (`NERVE_AUTH=true`).
This allows the browser UI to connect without having to manually enter or store the gateway token in the browser's persistent storage. If a connection is not trusted (e.g., remote access without authentication), the token field in the UI must be filled manually.
@ -171,7 +171,7 @@ Xiaomi MiMo is available as an explicit provider option when `MIMO_API_KEY` is s
| Variable | Default | Description |
|----------|---------|-------------|
| `STT_PROVIDER` | `local` | STT provider: `local` (whisper.cpp, no API key needed) or `openai` (requires `OPENAI_API_KEY`) |
| `WHISPER_MODEL` | `tiny` | Local whisper model: `tiny` (75 MB), `base` (142 MB), or `small` (466 MB) — multilingual variants. English-only variants (`tiny.en`, `base.en`, `small.en`) are also available. |
| `WHISPER_MODEL` | `base` | Local whisper model: `tiny` (75 MB), `base` (142 MB), or `small` (466 MB) — multilingual variants. English-only variants (`tiny.en`, `base.en`, `small.en`) are also available. |
| `WHISPER_MODEL_DIR` | `~/.nerve/models` | Directory for downloaded whisper model files |
| `NERVE_LANGUAGE` | `en` | Preferred voice language (ISO 639-1). Legacy `LANGUAGE` is still accepted but deprecated |
| `EDGE_VOICE_GENDER` | `female` | Edge TTS voice gender: `female` or `male` |
@ -179,7 +179,7 @@ Xiaomi MiMo is available as an explicit provider option when `MIMO_API_KEY` is s
```bash
# Use local speech-to-text (no API key needed)
STT_PROVIDER=local
WHISPER_MODEL=tiny
WHISPER_MODEL=base
NERVE_LANGUAGE=en
```
@ -285,7 +285,6 @@ REPLICATE_BASE_URL=https://api.replicate.com/v1
| `USAGE_FILE` | `~/.openclaw/token-usage.json` | Persistent cumulative token usage data |
| `NERVE_VOICE_PHRASES_PATH` | `~/.nerve/voice-phrases.json` | Override location for per-language voice phrase overrides |
| `NERVE_WATCH_WORKSPACE_RECURSIVE` | `false` | Re-enables recursive `fs.watch` for full workspace `file.changed` SSE events outside `MEMORY.md` and `memory/`. Disabled by default to prevent Linux inotify `ENOSPC` watcher exhaustion. Memory watchers stay enabled for discovered agent workspaces even when this is `false`. |
| `WORKSPACE_ROOT` | *(auto-detected)* | Allowed base directory for git workdir registration. Auto-derived from `git worktree list` or parent of `process.cwd()` |
```bash
FILE_BROWSER_ROOT=/home/user
@ -352,7 +351,7 @@ curl -X PUT http://localhost:3080/api/kanban/config \
| `allowDoneDragBypass` | `boolean` | `false` | Allow dragging tasks directly to done (skipping review) |
| `quickViewLimit` | `number` | `5` | Max tasks shown in workspace quick view (1--50) |
| `proposalPolicy` | `string` | `"confirm"` | How agent proposals are handled: `"confirm"` (manual review) or `"auto"` (apply immediately) |
| `defaultModel` | `string` | *(none)* | Default model for agent execution (max 100 chars). Falls back to `anthropic/claude-sonnet-4-5` |
| `defaultModel` | `string` | *(none)* | Default model for agent execution (max 100 chars). If unset, execution falls back to OpenClaw's configured default model |
### Column Schema
@ -438,7 +437,7 @@ MIMO_API_KEY=sk-mimo-...
# Speech / Language
STT_PROVIDER=local
WHISPER_MODEL=tiny
WHISPER_MODEL=base
NERVE_LANGUAGE=en
EDGE_VOICE_GENDER=female

View file

@ -68,7 +68,7 @@ On the cloud host config:
```json
"gateway": {
"tools": {
"allow": ["cron", "gateway"]
"allow": ["cron", "gateway", "sessions_spawn"]
}
}
```

View file

@ -1,34 +1,30 @@
# Nerve Documentation
# Nerve Docs
Use this folder as the docs hub for Nerve.
Start here when you need setup, operations, or contributor guidance.
## Core Docs
## Top-level entry points
- [Architecture](./ARCHITECTURE.md)
- [Configuration](./CONFIGURATION.md), setup wizard, auth, access modes, TTS providers, and appearance settings
- [API](./API.md)
- [Security](./SECURITY.md)
- [Troubleshooting](./TROUBLESHOOTING.md)
- [Updating](./UPDATING.md)
- [Installer Steps](./INSTALLER-STEPS.md)
- [Agent Markers](./AGENT-MARKERS.md)
- [Code Review](./CODE_REVIEW.md)
- [Project README](../README.md), product overview and quick start
- [Contributing](../CONTRIBUTING.md), local development, tests, and PR expectations
- [Changelog](../CHANGELOG.md), release notes
## Agent-Driven Setup
## Core docs
- [Architecture](./ARCHITECTURE.md), codebase structure and system design
- [Configuration](./CONFIGURATION.md), `.env`, auth, access modes, TTS providers, and UI settings
- [API](./API.md), backend endpoints and behavior
- [Security](./SECURITY.md), threat model and hardening notes
- [Troubleshooting](./TROUBLESHOOTING.md), common failures and fixes
- [Updating](./UPDATING.md), built-in updater flow and rollback
- [Installer Steps](./INSTALLER-STEPS.md), what the installer does
- [Agent Markers](./AGENT-MARKERS.md), TTS, charts, and kanban markers
- [Code Review](./CODE_REVIEW.md), review guidance for the current codebase
## Setup and deployment
- [AI Agent Setup](./AI_SETUP.md)
- [Nerve Agent Install Contract](./INSTALL.md)
## Deployment Guides
- [Run everything on one machine](./DEPLOYMENT-A.md)
- [Use a cloud Gateway with Nerve on your laptop](./DEPLOYMENT-B.md)
- [Run both Nerve and Gateway in the cloud](./DEPLOYMENT-C.md)
## How-to Guides
- [Add Tailscale to an existing Nerve install](./TAILSCALE.md)
## Release Notes
- [Changelog](../CHANGELOG.md)

View file

@ -227,7 +227,6 @@ All POST/PUT endpoints validate request bodies with [Zod](https://zod.dev/) sche
| `PUT /api/memories/section` | `title` (1200), `content` (≤50000), `date` (YYYY-MM-DD regex) |
| `DELETE /api/memories` | `query` (11000), `type` (enum), `date` (YYYY-MM-DD regex) |
| `PUT /api/workspace/:key` | `content` (string, ≤100 KB), `key` checked against strict allowlist |
| `POST /api/git-info/workdir` | `sessionKey` (non-empty), `workdir` (non-empty, validated against allowed base) |
Validation errors return **HTTP 400** with the first Zod issue message as plain text or JSON.
@ -295,7 +294,7 @@ This prevents the proxy from being used to connect to arbitrary external hosts.
### Token Injection
Nerve performs **server-side token injection** to provide a zero-config connection experience for local and authenticated users without exposing the `GATEWAY_TOKEN` to the browser storage.
Nerve performs **server-side token injection** to provide a zero-config connection experience for local and authenticated users without exposing the `GATEWAY_TOKEN` to the browser storage. Trusted-proxy configuration only affects how Nerve interprets forwarded client IPs, for example for loopback detection and rate limiting. It does not grant authentication by itself.
**Injection Logic:**
1. `GET /api/connect-defaults` returns the official gateway WebSocket URL, `token: null`, and a `serverSideAuth` flag.
@ -314,14 +313,15 @@ OpenClaw 2026.2.19+ requires a signed device identity (Ed25519 keypair) for WebS
Nerve generates a persistent device identity on first start (stored at `~/.nerve/device-identity.json`) and injects it into the connect handshake. The gateway always stays on loopback (`127.0.0.1`) — Nerve proxies all external connections through its WS proxy.
**First-time pairing (required once):**
**Normal setup path:** the setup wizard now pre-pairs Nerve's device identity while it is configuring the gateway, so a fresh install usually does **not** require a manual `openclaw devices approve` step.
**Manual approval is fallback / recovery guidance:**
1. Start Nerve and open the UI in a browser
2. The first connection creates a pending pairing request on the gateway
3. Approve it: `openclaw devices list``openclaw devices approve <requestId>`
4. All subsequent connections are automatically authenticated
2. If the device is still pending, list requests: `openclaw devices list`
3. Approve the Nerve device: `openclaw devices approve <requestId>`
If the device is rejected (e.g. after a gateway reset), the proxy falls back to token-only auth. The connection succeeds but with reduced scopes — chat and tool calls may fail with "missing scope" errors. Re-approve the device to restore full functionality.
If the device is rejected (for example after a gateway reset), the proxy falls back to token-only auth. The connection succeeds but with reduced scopes, and chat or tool calls may fail with "missing scope" errors until the device is approved again.
**Architecture:** `Browser (remote) → Nerve (0.0.0.0:3080) → WS proxy → Gateway (127.0.0.1:18789)`. The gateway never needs to bind to LAN or be directly network-accessible.
@ -353,7 +353,6 @@ Multiple layers prevent directory traversal attacks:
| `/api/files` | `path.resolve()` + prefix allowlist + symlink resolution + re-check |
| `/api/memories` (date params) | Regex validation: `/^\d{4}-\d{2}-\d{2}$/` — prevents injection in file paths |
| `/api/workspace/:key` | Strict key→filename allowlist (`soul`→`SOUL.md`, etc.) — no user-controlled paths |
| `/api/git-info/workdir` | Resolved path checked against allowed base directory (derived from git worktrees or `WORKSPACE_ROOT`). Exact match or child-path check with separator guard |
---
@ -403,7 +402,7 @@ The setup wizard:
1. Writes `.env` atomically (via temp file + rename)
2. Applies `chmod 600` to `.env` and backup files
3. Cleans up `.env.tmp` on interruption (Ctrl+C handler)
4. Backs up existing `.env` before overwriting (timestamped `.env.bak.*`)
4. Backs up existing `.env` before overwriting (`.env.backup` or `.env.backup.YYYY-MM-DD`)
---

View file

@ -129,7 +129,7 @@ The server detects `EADDRINUSE` and exits with a clear error (see `server/index.
**Symptom:** Red reconnecting banner appears periodically.
**Cause:** WebSocket connection to gateway dropped. Nerve auto-reconnects with exponential backoff (1s base, 30s max, up to 50 attempts).
**Cause:** WebSocket connection to gateway dropped. Nerve auto-reconnects with exponential backoff (1s base, 30s max).
**Diagnosis:**
```bash
@ -321,7 +321,7 @@ After approval, reconnect from the browser (refresh the page or click reconnect)
**Fix (local STT):**
- Models auto-download on first use. Check server logs for download progress or errors
- Ensure `ffmpeg` is installed (the installer handles this): `ffmpeg -version`
- Check model file exists: `ls ~/.nerve/models/ggml-tiny.bin`
- Check model file exists: `ls ~/.nerve/models/ggml-base.bin`
**Fix (OpenAI STT):**
- Set `STT_PROVIDER=openai` and `OPENAI_API_KEY` in `.env`
@ -336,7 +336,7 @@ After approval, reconnect from the browser (refresh the page or click reconnect)
**Causes:**
- Language is set incorrectly
- Local model is `tiny` (fast, but less accurate for conversational non-English)
- Local model is set to a smaller low-accuracy model
- English-only model (`*.en`) selected for non-English speech
**Fix:**
@ -427,7 +427,7 @@ MEMORY_PATH=/path/to/.openclaw/workspace/MEMORY.md
**Symptom:** "Timed out waiting for subagent to spawn" error.
**Cause:** Spawning uses a polling approach sends a `[spawn-subagent]` chat message to the selected root session, then polls `sessions.list` every 2s for up to 30s waiting for a new subagent session to appear.
**Cause:** Spawning uses a polling approach, sends a `[spawn-subagent]` chat message to the selected root session, then polls `sessions.list` every 1s for up to 60s waiting for a new subagent session to appear.
**Fix:**
- The selected root agent must be running and able to process the spawn request
@ -438,12 +438,17 @@ MEMORY_PATH=/path/to/.openclaw/workspace/MEMORY.md
**Symptom:** A Kanban task enters `in-progress`, but the worker session never links up cleanly, or completion only works by label fallback.
**Cause:** Kanban execution intentionally avoids HTTP `/tools/invoke -> sessions_spawn` and instead uses the gateway's session-native path: `chat.send` with `[spawn-subagent]`, followed by `sessions.list` discovery. That means Kanban execution depends on a usable top-level root session and on the gateway being able to surface the new child session.
**Cause:** Kanban has two execution paths now:
- **Assigned tasks** run beneath the assignee's live root session via RPC `[spawn-subagent]`.
- **Unassigned or `operator` tasks** use the normal `sessions_spawn` path.
That means failures can come from either a missing assignee root, a delayed child discovery step, or the normal subagent spawn path. On macOS, unassigned or `operator` tasks are rejected outright and must be assigned to a live worker root first.
**Fix:**
- Make sure a top-level root session such as `agent:main:main` exists and is healthy
- Check gateway RPC/session logs, not just HTTP tool logs
- If child discovery is delayed, Kanban falls back to the human-readable run label for completion tracking
- If the task is assigned, make sure that assignee's root session exists and is healthy
- If the task is unassigned, verify the normal `sessions_spawn` path is working
- On macOS, assign the task to a live worker root before executing it
- Check gateway RPC/session logs for the assignee-root path, and HTTP tool logs for the normal spawn path
- If the worker never appears in the session list, inspect gateway connectivity and recent session events first
### Session status stuck on "THINKING"
@ -465,24 +470,28 @@ MEMORY_PATH=/path/to/.openclaw/workspace/MEMORY.md
**Symptom:** Model selector is empty or shows only the current model.
**Cause:** Models are fetched via `GET /api/gateway/models`. Nerve first runs `openclaw models list --json` for configured / allowlisted models, waits up to 15 seconds for cold starts, and falls back to `openclaw models list --all --json` if needed. If those CLI calls fail or return nothing, the dropdown can stay sparse.
**Cause:** Models are fetched via `GET /api/gateway/models`, which reads the active OpenClaw config. If that config is unreadable, or it has no configured models, the dropdown can stay empty or sparse.
**Fix:**
- Wait a moment after a cold start, then reopen the spawn dialog or refresh the page
- Ensure the `openclaw` binary is in PATH (the server searches multiple locations — see `lib/openclaw-bin.ts`)
- Set `OPENCLAW_BIN` env var to the explicit path
- Check server logs for model list errors or timeouts
- If you use an allowlist, verify the expected Codex or other models are configured there
- Verify the expected models are configured in OpenClaw (`agents.defaults.model` / `agents.defaults.models`)
- Check that Nerve can read the active OpenClaw config file
- Check server logs for `gateway/models` read errors
- After fixing config, reopen the spawn dialog or refresh the page
### Model change doesn't take effect
**Symptom:** Switched model in UI but responses still come from the old model.
**Cause:** Model/thinking changes go through `POST /api/gateway/session-patch`, which invokes the gateway's session patch API.
**Cause:** There are two different paths now:
- **Model changes** can fall back to `POST /api/gateway/session-patch`
- **Thinking changes** must go through WebSocket RPC `sessions.patch`
If you call the HTTP fallback with only `thinkingLevel`, it returns **501**. If Nerve cannot find an active root session and you omitted `sessionKey`, it can return **409**.
**Fix:**
- The change applies per-session — switching sessions will show that session's model
- Verify the patch succeeded: check for `{ ok: true }` response
- For model changes, verify the HTTP fallback returned `{ ok: true }`
- For thinking changes, retry after the WebSocket reconnects and use the normal `sessions.patch` flow
- If you see a 409 from the HTTP fallback, pass `sessionKey` explicitly or make sure an active root session exists
- Some models may not be available for the current session type
---

View file

@ -2,6 +2,20 @@
Nerve ships a built-in updater that pulls the latest published release from GitHub, rebuilds, restarts the service, and verifies health — all in one command.
## Prerequisites
Before using the updater, make sure this checkout has an HTTPS GitHub `origin`, for example:
```bash
git remote -v
```
`origin` should point to `https://github.com/<owner>/<repo>.git`. If it does not, fix it first:
```bash
git remote set-url origin https://github.com/<owner>/<repo>.git
```
## Quick start
```bash
@ -9,7 +23,7 @@ npm run update -- --yes
```
This will:
1. Check prerequisites (git, Node.js, npm)
1. Check prerequisites (git, Node.js, npm, and an HTTPS GitHub `origin` remote)
2. Resolve the latest published GitHub release (fallback: latest semver tag)
3. Snapshot the current state for rollback
4. `git fetch --tags && git checkout <tag>`
@ -143,8 +157,9 @@ The updater resolves versions from GitHub Releases first. If release lookup fail
**Fix:** Verify remote/release access and tags:
```bash
git remote -v # Verify origin points to the right repo
git fetch --tags origin # Pull any missing tags
git remote -v
git remote set-url origin https://github.com/<owner>/<repo>.git
git fetch --tags origin
curl -sSf https://api.github.com/repos/<owner>/<repo>/releases/latest | jq .tag_name
```