mirror of
https://github.com/daggerhashimoto/openclaw-nerve
synced 2026-04-21 10:37:17 +00:00
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:
parent
b205b7f654
commit
808a5fcc8e
12 changed files with 368 additions and 442 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
170
docs/API.md
170
docs/API.md
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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()`.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ On the cloud host config:
|
|||
```json
|
||||
"gateway": {
|
||||
"tools": {
|
||||
"allow": ["cron", "gateway"]
|
||||
"allow": ["cron", "gateway", "sessions_spawn"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -227,7 +227,6 @@ All POST/PUT endpoints validate request bodies with [Zod](https://zod.dev/) sche
|
|||
| `PUT /api/memories/section` | `title` (1–200), `content` (≤50000), `date` (YYYY-MM-DD regex) |
|
||||
| `DELETE /api/memories` | `query` (1–1000), `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`)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue