n2n-nexus 0.8.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/CONTRIBUTING.md +49 -0
  3. package/README.md +157 -77
  4. package/SECURITY.md +37 -0
  5. package/build/cli/client-command.js +58 -0
  6. package/build/cli/doctor.js +58 -8
  7. package/build/cli/entry.js +5 -0
  8. package/build/cli/hook.js +104 -16
  9. package/build/cli/install.js +37 -13
  10. package/build/cli/mcp-config.js +16 -12
  11. package/build/client/discovery.js +115 -0
  12. package/build/client/nexus-client.js +8 -2
  13. package/build/contract.js +69 -0
  14. package/build/index.js +12 -18
  15. package/build/server/nexus.js +63 -35
  16. package/contract/errors.json +52 -0
  17. package/contract/hook.json +156 -0
  18. package/contract/tools.json +2812 -0
  19. package/docs/ARCHITECTURE.md +142 -108
  20. package/docs/ARCHITECTURE_zh.md +144 -110
  21. package/docs/ASSISTANT_GUIDE.md +5 -4
  22. package/docs/ASSISTANT_GUIDE_zh.md +134 -0
  23. package/docs/MEETING_MINUTES_2025-12-29.md +166 -0
  24. package/docs/README_zh.md +159 -79
  25. package/docs/TOOLS_REFERENCE.md +404 -0
  26. package/docs/discussion_2025-12-29.md +727 -0
  27. package/llms.txt +16 -5
  28. package/package.json +12 -8
  29. package/build/cli/autostart.js +0 -64
  30. package/build/config/cli.js +0 -8
  31. package/build/config/paths.js +0 -38
  32. package/build/constants.js +0 -18
  33. package/build/daemon/hook-contract.js +0 -68
  34. package/build/daemon/index.js +0 -57
  35. package/build/daemon/rate-limit.js +0 -62
  36. package/build/daemon/server.js +0 -862
  37. package/build/daemon/tool-outputs.js +0 -180
  38. package/build/daemon/tools.js +0 -326
  39. package/build/storage/agents.js +0 -227
  40. package/build/storage/docs.js +0 -81
  41. package/build/storage/events.js +0 -31
  42. package/build/storage/fs-utils.js +0 -88
  43. package/build/storage/global-messages.js +0 -44
  44. package/build/storage/index.js +0 -141
  45. package/build/storage/migrations.js +0 -87
  46. package/build/storage/paths.js +0 -23
  47. package/build/storage/projects.js +0 -53
  48. package/build/storage/registry.js +0 -355
  49. package/build/storage/sqlite-meeting.js +0 -323
  50. package/build/storage/sqlite.js +0 -275
  51. package/build/storage/tasks.js +0 -138
  52. package/build/types.js +0 -1
  53. package/build/utils/async-rwlock.js +0 -64
@@ -4,33 +4,40 @@
4
4
 
5
5
  ```
6
6
  ┌──────────────────────────────────────────────────┐
7
- │ n2n-nexus daemon │
8
- │ Standalone HTTP server · started by the user, │
9
- │ or in the background by a local hook / adapter │
7
+ │ n2n-nexus-host (Go) │
8
+ │ Tray app, or a boot service (`service install`) │
10
9
  │ │
11
- │ ┌──────────────┐ ┌──────────────────────────┐ │
12
- │ │ REST API │ │ Storage Layer │ │
13
- │ │ /api/tools │ │ SQLite (WAL) + JSON FS │ │
14
- │ │ /api/tools │ │ │ │
15
- │ │ /call │ │ Single writer — no lock │ │
16
- │ └──────┬───────┘ └──────────────────────────┘ │
17
- │ All business logic, tool definitions, data I/O │
10
+ │ ┌──────────────────┐ ┌──────────────────────┐ │
11
+ │ │ HTTP API │ │ Storage │ │
12
+ │ │ GET /health │ │ one SQLite file │ │
13
+ │ │ GET /api/tools │ │ (nexus.db, WAL), │ │
14
+ │ │ POST /api/tools │ │ numbered migrations,│ │
15
+ │ │ /call │ │ one transaction │ │
16
+ │ │ POST /api/hook │ │ per write │ │
17
+ │ └──────┬───────────┘ └──────────────────────┘ │
18
+ │ Tools, conversations, LAN discovery, cleanup │
18
19
  └─────────┼────────────────────────────────────────┘
19
20
  │ HTTP (NEXUS_ENDPOINT — cross-env capable)
20
21
  ┌──────┼──────┐
21
22
  ▼ ▼ ▼
22
23
  MCP-A MCP-B MCP-C hooks (Claude Code / Codex)
23
24
  (Win) (WSL) (VM) POST /api/hook per client event
24
- Lightweight protocol adapter, one per MCP connection
25
+ n2n-nexus (npm): stateless adapter, one per MCP connection
25
26
  ```
26
27
 
28
+ Two programs share one protocol:
29
+
30
+ - **`n2n-nexus-host`** (Go, `host/`): owns all data, tool definitions and business logic. Installed from GitHub Releases, Homebrew or Scoop.
31
+ - **`n2n-nexus`** (TypeScript, npm, `src/`): the client only — the MCP adapter (`mcp`), the hook command (`hook`), `install` / `uninstall` for Claude Code and Codex, and `doctor`. It contains no storage and no server.
32
+ - **`contract/`** is the single source of the protocol between them: `tools.json` (every tool's name, title, description, annotations, input and output JSON Schema), `hook.json` (hook fields, `_meta` keys, timings, limits, notification texts) and `errors.json` (endpoints, error body, status codes). The host embeds these files, the client reads `hook.json` at run time, and the conformance tests in `tests/conformance/` check a host against them. `GET /health` reports the host's `contractVersion`; `n2n-nexus doctor` compares it with the client's.
33
+
27
34
  ### Core Principles
28
35
 
29
- 1. **Daemon is the single source of truth** — all reads/writes, business logic, and tool definitions live in the daemon.
30
- 2. **MCP is stateless** — no hardcoded tool names, no local persistent data and no session of its own; conversations and direct messages live in the daemon.
31
- 3. **Daemon decides tool capability** — MCP fetches `GET /api/tools` at startup and reconnect.
32
- 4. **No local fallback** — if the daemon is unreachable, the MCP reports an error and retries every 3 s. No split-brain degradation. A tool call the daemon rejects (validation, not found, permission) is returned to the IDE as an error result (`isError: true`) and does not affect the connection; an unknown tool name is a JSON-RPC `-32602` error.
33
- 5. **Plain HTTP between MCP, hooks and daemon** — the client's hooks report conversation events and carry new-message notifications back to the model; the model reads the text with a tool.
36
+ 1. **The host is the single source of truth** — all reads/writes, business logic, and tool definitions live in the host.
37
+ 2. **MCP is stateless** — no hardcoded tool names, no local persistent data and no session of its own; conversations and direct messages live in the host.
38
+ 3. **The host decides tool capability** — the adapter fetches `GET /api/tools` at startup and reconnect.
39
+ 4. **No local fallback** — if the host is unreachable, the MCP reports an error and retries every 3 s. No split-brain degradation. A tool call the host rejects (validation, not found, permission) is returned to the IDE as an error result (`isError: true`) and does not affect the connection; an unknown tool name is a JSON-RPC `-32602` error.
40
+ 5. **Plain HTTP between MCP, hooks and host** — the client's hooks report conversation events and carry new-message notifications back to the model; the model reads the text with a tool.
34
41
 
35
42
  ### MCP Startup Flow
36
43
 
@@ -43,20 +50,22 @@ Read NEXUS_ENDPOINT (default: http://127.0.0.1:5688)
43
50
  │ │ list_changed delivered on subscriptions/listen
44
51
  │ └─ 2025-era clients: initialize handshake, unsolicited list_changed
45
52
  │
46
- ├─ Connection refused at 127.0.0.1 / localhost: start a daemon in the background once
53
+ ├─ Nothing answers: say how to start n2n-nexus-host on this machine,
54
+ │ or on the remote machine for a remote endpoint; never start one
55
+ ├─ A call not answered in time fails alone: tool list kept, no reconnect
47
56
  │
48
- ├─ If the daemon was down: retry loop (every 3 s)
57
+ ├─ If the host was down: retry loop (every 3 s)
49
58
  │ └─ GET /api/tools succeeds → cache tool list
50
59
  │ → notifications/tools/list_changed → IDE re-fetches
51
60
  │
52
61
  └─ stdin closes → stop retrying, close, exit
53
62
  ```
54
63
 
55
- The adapter's instance ID (read cursors, meeting permissions) uses the first client name seen on the connection, from the request's `io.modelcontextprotocol/clientInfo` (2026-07-28) or from `initialize` (2025 era), unless `NEXUS_INSTANCE_ID` / `--id` pins it.
64
+ The adapter's instance ID (read cursors, meeting permissions) uses the first client name seen on the connection, from the request's `io.modelcontextprotocol/clientInfo` (2026-07-28) or from `initialize` (2025 era), unless `NEXUS_INSTANCE_ID` / `--id` pins it. Every tool call carries it as `instanceId`; `read_messages` refuses a call without one, since unread cursors are kept per instance.
56
65
 
57
66
  ### Conversations and hooks
58
67
 
59
- Each Claude Code or Codex conversation is one session, `claude:<session id>` or `codex:<session id>`, using the ID the client itself assigns. `n2n-nexus install <claude|codex>` writes hooks that run `n2n-nexus hook <client>` on SessionStart, UserPromptSubmit, PostToolUse, Stop and SessionEnd (and, for Claude Code, PreToolUse on the n2n-nexus tools). The hook command forwards the event's identifying fields to `POST /api/hook` and prints the daemon's answer; on any error it exits 0 without output, so a missing daemon never blocks the client.
68
+ Each Claude Code or Codex conversation is one session, `claude:<session id>` or `codex:<session id>`, using the ID the client itself assigns. `n2n-nexus install <claude|codex>` writes hooks that run `n2n-nexus hook <client>` on SessionStart, UserPromptSubmit, PostToolUse, Stop and SessionEnd (and, for Claude Code, PreToolUse on the n2n-nexus tools). The hook command forwards the event's identifying fields to `POST /api/hook` and prints the host's answer; on any error it exits 0 without output, so a missing host never blocks the client.
60
69
 
61
70
  ```
62
71
  Client event ──▶ n2n-nexus hook ──▶ POST /api/hook ──▶ session status, notifications
@@ -67,36 +76,56 @@ Stop ◀── decision: block ◀──┘ once pe
67
76
 
68
77
  Tool calls name their conversation outside the model's arguments. The adapter reads it from the request's `_meta`: Claude Code sends `claudecode/toolUseId`, which the PreToolUse hook bound to the conversation just before the call (verified: the call waits for the hook); Codex sends `sessionId`. The adapter forwards it as `caller` in `POST /api/tools/call`. Because the identity is the conversation, not the adapter process, an adapter restart, a resumed conversation and a sub-agent all keep it, and one adapter can serve several Codex conversations.
69
78
 
70
- Notifications carry the sender and the first line only; the text reaches the model through `read`, as tool output rather than injected context. A listening Stop hook is held open for up to 590 s and answered as soon as a message arrives; the client killing the hook (the user pressing Esc) or the daemon shutting down ends the wait. This remains a trusted local daemon, not an authentication boundary: every process of the same operating-system user is trusted and could act as any conversation, and agents that must be isolated run as different users with their own daemons (see the local trust model in [SECURITY.md](../SECURITY.md)). See [tool reference](TOOLS_REFERENCE.md#direct-messages-between-conversations).
79
+ Notifications carry the sender and the first line only; the text reaches the model through `read`, as tool output rather than injected context. A listening Stop hook is held open for up to 590 s and answered as soon as a message arrives; the client killing the hook (the user pressing Esc) or the host shutting down ends the wait. This remains a trusted local host, not an authentication boundary: every process of the same operating-system user is trusted and could act as any conversation, and agents that must be isolated run as different users with their own hosts (see the local trust model in [SECURITY.md](../SECURITY.md)). See [tool reference](TOOLS_REFERENCE.md#direct-messages-between-conversations).
71
80
 
72
- ---
81
+ ### Sessions, presence and waking idle conversations
73
82
 
74
- ## 💾 Data Persistence
83
+ A new conversation's SessionStart hook prints an introduction: its own name and ID, the other sessions that are not offline (up to five) and how to use `peers`, `send` and `read`. The adapter also sends MCP server instructions with the same guidance. Clients do not always report the end of a conversation, so an `idle` or `working` session without activity for 30 minutes counts as `offline` until its next event.
75
84
 
76
- All data lives under the daemon's storage root (default `~/.n2n-nexus`):
85
+ An idle conversation is not running, so hooks alone can only reach it when its user acts. To let AI sessions answer each other without anyone typing, `install` adds a second Stop hook, `n2n-nexus hook <client> --wait`, that starts a background wait when a turn ends:
77
86
 
78
87
  ```
79
- ~/.n2n-nexus/
80
- ├── global/
81
- │ ├── blueprint.md # Master strategy document
82
- │ ├── docs_index.json # Global docs index
83
- │ └── docs/ # Shared markdown documents
84
- ├── projects/
85
- │ └── {project-id}/
86
- │ ├── manifest.json # Project metadata
87
- │ ├── internal_blueprint.md # Internal technical docs
88
- │ └── assets/ # Binary assets (images, PDFs)
89
- ├── registry.json # Global project index
90
- └── nexus.db # SQLite database (meetings, global chat, events, tasks, cursors, conversations, direct messages)
88
+ turn ends ──▶ Stop hook (announces unread, or listens)
89
+ └─▶ Stop hook --wait ──▶ POST /api/hook {BackgroundWait} (up to 590 s per request, repeated for up to 1 h)
90
+ │ session shows as "listening"
91
+ ├─ new message ─▶ { wake: notification }
92
+ │ Claude Code: asyncRewake hook exits 2, the notification starts a turn
93
+ │ Codex: detached process runs `codex queue --thread <id> --message <notification>`
94
+ ├─ any other event of the session ─▶ null (wait ends)
95
+ └─ time runs out ─▶ { timeout: true } (ask again)
91
96
  ```
92
97
 
93
- **SQLite WAL mode**: Only the daemon process opens the database. MCP adapters never touch it — they go through HTTP.
98
+ The wait never delivers while the session is `working` or while a listening Stop hook is waiting, so a message is announced once. Verified on Windows with the Claude desktop app and the Codex desktop app (2026-10-23): both directions wake an idle conversation within seconds. Known limits:
99
+
100
+ - Codex starts a queued turn only for a conversation loaded in the running Codex app. After Codex restarts, a conversation is loaded once the user opens it.
101
+ - The Codex background wait ends when the Codex app quits, and starts again only when the conversation ends a turn. Messages sent meanwhile stay unread in the host (not in Codex's own queue) until the user next prompts that conversation.
102
+ - A Claude Code conversation is reachable for an hour after its last turn (the hook timeout).
103
+ - Every AI reply wakes the recipient again. To stop two sessions waking each other forever, the host counts automatic turns (wakes, listening Stops, Stops held open for messages) since the user's last prompt and stops after `maxAutoTurns` in `settings.json` (default 8); a prompt within a minute of a wake is the woken turn starting and keeps the count. Messages past the limit wait until the user prompts.
104
+ - The automatic turn count tells the woken turn from the user by time alone. If a wake does not start a turn (a Codex conversation not loaded) and the user prompts within that minute, that prompt keeps the count; the user's next prompt resets it. If a woken turn starts more than a minute late (a Codex conversation the user opens later), its prompt resets the count. Neither loses a message or lets sessions loop without a user.
105
+
106
+ ---
107
+
108
+ ## 💾 Data Persistence
109
+
110
+ All data lives in one SQLite file, `nexus.db`, in the host's data directory: `%LOCALAPPDATA%\n2n-nexus` on Windows, `~/Library/Application Support/n2n-nexus` on macOS, `$XDG_DATA_HOME/n2n-nexus` (default `~/.local/share/n2n-nexus`) on Linux; `--root` or `NEXUS_ROOT` overrides it. The same directory holds `settings.json` and `host.lock`.
111
+
112
+ - **Schema**: numbered migrations embedded in the host (`host/internal/store/migrations/0001_core.sql`, `0002_agents.sql`) run in order at startup, each in one transaction; the version is `PRAGMA user_version`. A database newer than the host is refused ("upgrade n2n-nexus-host").
113
+ - **Tables**: `projects` (scalar manifest fields as columns, structured fields as JSON, internal docs as text), `project_relations` and `project_assets` (asset bytes as BLOBs; both cascade on project rename and delete), `global_strategy`, `global_docs`, `meetings` (with `status` and `is_default`), `meeting_messages`, `decisions`, `global_messages`, `events`, `tasks`, `read_cursors`, and for conversations `agent_sessions`, `agent_messages`, `agent_notices`, `agent_idle_subscriptions`, `tool_use_bindings`.
114
+ - **Transactions**: every write is one transaction, so a multi-row change (a meeting message, starting / ending / reopening a meeting, renaming a project) is applied completely or not at all. The database runs in WAL mode with `foreign_keys` on and a 5 s busy timeout, through a single connection.
115
+ - **Only the host opens the database**. MCP adapters and hooks go through HTTP.
116
+ - **Activity log**: state changes made through tools (project sync/update/rename/delete/asset, doc sync, strategy update, meeting start/end/archive/reopen, maintenance) are recorded as structured events and read with `list_events`; they are not mixed into chat streams.
117
+ - **Old data**: the file-based storage of n2n-nexus 0.x (`~/.n2n-nexus`) is not imported or read, and is left untouched.
118
+
119
+ ### Maintenance and retention
94
120
 
95
- **File-store access**: HTTP requests are handled concurrently, so every mutation of `registry.json`, manifests and the docs index holds one daemon-wide read-write lock alone and is written atomically (temp file + rename); reads share the lock, so a rename or update is never observed half-done. A write orders its files so that `registry.json` goes last and restores what it had already written when a later step fails.
121
+ At startup and then every hour the host:
96
122
 
97
- **Activity log**: state changes made through the daemon (project sync/rename/delete, meeting start/end, doc sync, ...) are recorded as structured events in SQLite and read with `list_events` / `GET /api/events`; they are not mixed into chat streams.
123
+ - marks pending or running tasks past their TTL as `failed` and deletes tasks 7 days after they finished (`completed`, `failed`, `cancelled`);
124
+ - deletes events older than 30 days;
125
+ - expires unread direct messages past their 24-hour lifetime (telling the senders), and deletes read or expired direct messages and delivered notices after 30 days;
126
+ - deletes tool-use bindings once they have expired (5 minutes).
98
127
 
99
- **Self-healing**: `registry.json` is auto-repaired if corrupted or missing.
128
+ Logs: the service, and the tray while it runs the host in-process, write `host.log` in the log directory (`%LOCALAPPDATA%\n2n-nexus\logs`, `~/Library/Logs/n2n-nexus`, `$XDG_STATE_HOME/n2n-nexus` with default `~/.local/state/n2n-nexus`), rotated at 10 MB with 3 old files kept; only the holder of `host.lock` writes it. The tray's own lines go to `tray.log` there, rotated the same way. `serve` writes to standard error.
100
129
 
101
130
  ---
102
131
 
@@ -124,24 +153,26 @@ All project IDs must follow the `[prefix]_[name]` format:
124
153
 
125
154
  ## 🌐 Deployment Model
126
155
 
127
- One npm package, two commands:
156
+ Two programs:
128
157
 
129
158
  ```
130
- n2n-nexus
131
- │
132
- ├─ n2n-nexus daemon Start once, keep running. Owns all data.
133
- ├─ n2n-nexus mcp IDE starts automatically via npx. Stateless proxy.
134
- ├─ n2n-nexus hook Run by Claude Code / Codex on each hook event.
135
- ├─ n2n-nexus install Write the hooks and MCP server into a client's user configuration.
136
- └─ n2n-nexus doctor Check that the installed hook and MCP commands still run.
159
+ n2n-nexus-host Install once per host machine. Owns all data.
160
+ ├─ (no arguments) Tray app; runs the host in-process, or only controls
161
+ │ a host already answering on the configured port
162
+ ├─ serve Foreground, no tray (debugging; what the service runs)
163
+ ├─ service install|uninstall|status
164
+ │ Boot service running as the installing user
165
+ ├─ autostart on|off Start the tray at login
166
+ └─ version
167
+
168
+ n2n-nexus (npm) Install on every machine with an IDE.
169
+ ├─ n2n-nexus mcp IDE starts automatically. Stateless proxy.
170
+ ├─ n2n-nexus hook Run by Claude Code / Codex on each hook event.
171
+ ├─ n2n-nexus install Write the hooks and MCP server into a client's user configuration.
172
+ └─ n2n-nexus doctor Check the installed commands and the host's contract version.
137
173
  ```
138
174
 
139
- **Daemon** (started by the user, or in the background by a hook or adapter when none listens at a local endpoint):
140
- ```bash
141
- npx n2n-nexus daemon --port 5688
142
- # or with explicit root
143
- npx n2n-nexus daemon --root ~/.n2n-nexus --port 5688
144
- ```
175
+ **Host**: started as a tray app or a boot service; the adapter and the hooks never start one, they report that none answers. Configuration comes only from `<data dir>/settings.json` (`host`, `port`, `discoveryPort`, `allowedHosts`, `rateLimit`, `rateBurst`, `maxAutoTurns`), overridden by command-line flags; the only environment variable the host reads is `NEXUS_ROOT`. A service does not get the environment of the terminal that installed it, so the file is the one source the tray, the service and the command line share.
145
176
 
146
177
  **MCP** (IDE configuration):
147
178
  ```json
@@ -156,80 +187,83 @@ npx n2n-nexus daemon --root ~/.n2n-nexus --port 5688
156
187
  }
157
188
  ```
158
189
 
159
- **No required startup order**: start the IDE before the daemon — tools appear automatically once the daemon comes up. Daemon restarts are transparent; MCP reconnects and notifies the IDE.
190
+ **Standalone host on a LAN**: run one host with LAN access on (tray **Allow LAN access**, or `n2n-nexus-host service install --host 0.0.0.0`), independent of any IDE, and allow TCP and UDP 5688 from the local subnet (on Windows the tray adds both firewall rules). Each client machine then needs only the client side: `n2n-nexus install <claude|codex> --endpoint discover` writes the MCP server and the hooks, which find the host by UDP broadcast. On Windows, run `npm install -g` from a regular terminal: a terminal inside a packaged (MSIX) app such as Codex installs into that app's private copy of `%APPDATA%`, which only that app sees and which hides a later global install from it.
191
+
192
+ **No required startup order**: start the IDE before the host — tools appear automatically once the host comes up. Host restarts are transparent; MCP reconnects and notifies the IDE.
193
+
194
+ ### Tray and service per OS
195
+
196
+ | | Windows | macOS | Linux |
197
+ | --- | --- | --- | --- |
198
+ | Boot service (`service install`, administrator / root) | Scheduled task `n2n-nexus-host`: at startup, S4U logon of the installing user (no password stored), no time limit | `/Library/LaunchDaemons/io.n2ns.n2n-nexus-host.plist` with `UserName`, `RunAtLoad`, `KeepAlive` | `/etc/systemd/system/n2n-nexus-host.service` with `User=`, `Restart=always`, `RestartSec=5` |
199
+ | Tray at login (`autostart`, tray menu) | `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` value `n2n-nexus-host` | `~/Library/LaunchAgents/io.n2ns.n2n-nexus-host.tray.plist` | `~/.config/autostart/n2n-nexus-host.desktop` |
200
+ | LAN access from the tray | Adds inbound TCP / UDP firewall rules through a UAC prompt (`netsh advfirewall`) | Tells the user which ports to allow | Tells the user which ports to allow |
201
+
202
+ Every service runs `n2n-nexus-host serve --supervise --root <data dir> --log-dir <log dir>` with the installing user's directories, so the service's own environment does not move them: the supervisor runs `serve` as a child process and starts it again 5 s after it exits, and logs to `host.log`. `service install` refuses while a tray runs the host for the data directory in-process (quit the tray first; opened again, it controls the service); otherwise it stops the host running for the data directory (found through the PIDs recorded in `host.lock`, never by matching command lines), writes `--host` / `--port` to `settings.json`, registers and starts the service and reports what answers on the port; under `sudo` it uses the invoking user's default directories. `service uninstall` removes the service and stops only the service's own `serve`, never a tray or a foreground `serve`. The tray first probes `GET /health` on the configured port: when a host answers (for example the service), it only controls that host — **Restart** (and a changed setting) writes `restart-request` into the data directory; the service's `serve` polls for it every second and exits, and its supervisor starts it with the new settings (a file, since the tray may not end a process of the scheduled task's S4U logon), then waits up to 20 s for it to answer again, never starting a host of its own — and otherwise it runs the host in-process. Changing a setting from the tray writes `settings.json` and restarts the host. The menu shows the address the host is bound to and the number of online sessions (`sessions.online` of `GET /health`), refreshed every 5 s. An IPv4 address (`0.0.0.0` included) listens on IPv4 only, an IPv6 address on IPv6 only. The tray's texts are Chinese on a Chinese system and English otherwise (on macOS without `LANG`, as under the login item, from `AppleLanguages`). The macOS tray needs cgo (Cocoa), so macOS release builds are built on macOS with `CGO_ENABLED=1`; Windows and Linux are cross-compiled with `CGO_ENABLED=0`, and Windows ships two builds of the same program: `n2n-nexus-host.exe` (console, for commands; run without arguments it starts the tray build next to it and returns) and `n2n-nexus-host-tray.exe` (GUI, `-H=windowsgui`, opens no console window; start at login registers it).
203
+
204
+ ### Single instance
205
+
206
+ A host takes an exclusive lock on `<data dir>/host.lock` before it opens the database and records its PID, its supervisor's and its role (`tray`, `serve` or `supervised`) there. A second host for the same data directory exits with "another n2n-nexus-host is running for <dir> (PID n)".
160
207
 
161
208
  ---
162
209
 
163
- ## 📡 REST API
210
+ ## 🧩 Host internals
164
211
 
165
- ### Tool capability (MCP-facing)
166
212
  ```
167
- GET /api/tools Return full tool definition list (JSON Schema)
168
- POST /api/tools/call Execute tool call { tool, args, instanceId, caller? }
169
- POST /api/hook Client hook event { client, input } → { output }
213
+ host/
214
+ ├── cmd/n2n-nexus-host/ command line: tray (default), serve, service, autostart, version
215
+ └── internal/
216
+ ├── config/ data and log directories, settings.json
217
+ ├── protocol/ loads the embedded contract (contract/contract.go), compiles the JSON Schemas,
218
+ │ strips undeclared argument keys, validates arguments and results
219
+ ├── store/ opens nexus.db, runs the migrations, transactions
220
+ ├── core/ project, doc, meeting, chat, event and task tools and their tables
221
+ ├── agents/ conversations: hooks, direct-message tools, waits, their tables
222
+ ├── api/ HTTP server: the 4 endpoints, Host / Origin guard, body limit, rate limits
223
+ ├── discovery/ UDP LAN discovery responder
224
+ ├── host/ assembles one host: lock, database, tools, server, discovery, hourly maintenance, logs
225
+ ├── tray/ tray menu and its English / Chinese texts
226
+ ├── service/ boot service, tray autostart, the `serve --supervise` supervisor, firewall script
227
+ ├── clock/ system clock and the settable test clock
228
+ └── testhooks/ test-build only (`-tags testclock`): POST /test/clock, --test-listen-wait-ms
170
229
  ```
171
230
 
172
- ### System
173
- ```
174
- GET /health Health status + version
175
- ```
231
+ The Go module is `github.com/n2ns/n2n-nexus` at the repository root, so the host embeds `contract/` directly. A tool exists only if it is in `contract/tools.json`; the host registers one handler per tool and logs any tool without a handler at startup.
176
232
 
177
- ### Projects
178
- ```
179
- POST /api/projects/sync
180
- POST /api/projects/update
181
- POST /api/projects/rename
182
- POST /api/projects/delete
183
- GET /api/projects/search
184
- GET /api/projects/topology
185
- GET /api/projects/:projectId
186
- ```
233
+ **Conversation state machine**: `agents.Agents` is the in-process coordinator of one host. It holds all in-memory conversation state — held listening Stops, background waits, automatic-turn counts, wake times — behind one mutex; every hook and direct-message tool call runs under it, and whoever stores a message or notice delivers it to a waiting hook before releasing the lock. At startup it resets sessions a previous host left `listening` to `idle`. Shutting down answers every held hook (`{ "output": null }`) before the server closes. Time comes from a `Clock`: the system clock in release builds, a clock the conformance tests set through `POST /test/clock` in the `testclock` build.
187
234
 
188
- ### Messages & Global
189
- ```
190
- POST /api/messages/send
191
- GET /api/messages/unread
192
- GET /api/events
193
- GET /api/global/docs
194
- GET /api/global/docs/:docId
195
- POST /api/global/docs/:docId
196
- GET /api/global/strategy
197
- POST /api/global/strategy
198
- ```
235
+ **Discovery**: a host whose `host` is not a loopback address answers `n2n-nexus/discover` datagrams on UDP `discoveryPort` with `{"service":"n2n-nexus","version","host","port"}`. Clients with `NEXUS_ENDPOINT=discover` broadcast the query on `NEXUS_DISCOVERY_PORT`, use the first answer and cache it in `~/.n2n-nexus/discovered-endpoint`.
199
236
 
200
- ### Meetings
201
- ```
202
- GET /api/meetings
203
- POST /api/meetings/start
204
- POST /api/meetings/end
205
- POST /api/meetings/archive
206
- POST /api/meetings/reopen
207
- GET /api/meetings/:meetingId
208
- ```
237
+ ---
209
238
 
210
- ### Tasks
211
- ```
212
- POST /api/tasks
213
- GET /api/tasks
214
- GET /api/tasks/:taskId
215
- POST /api/tasks/:taskId/update
216
- POST /api/tasks/:taskId/cancel
217
- ```
239
+ ## 📡 HTTP API
240
+
241
+ The host has four endpoints (see `contract/errors.json`):
218
242
 
219
- ### Maintenance
220
243
  ```
221
- POST /api/maintenance/logs
244
+ GET /health { ok, service: "n2n-nexus", version, contractVersion, sessions: { online } }
245
+ GET /api/tools Tool definitions from contract/tools.json (JSON Schema)
246
+ POST /api/tools/call { tool, args, instanceId, caller? } → { ok: true, result }
247
+ POST /api/hook Client hook event { client, input } → { output }; the BackgroundWait event is the background wait
222
248
  ```
223
249
 
224
- ---
250
+ Every capability is a tool; there are no other REST routes (an unknown route is 404). A tool call validates `args` against the tool's input schema after dropping keys the schema does not declare (400 `Invalid arguments for <tool>: ...`), and the result against its output schema. A failure the caller caused, including "not found", is 400 with the error text; an unknown tool is 404. Errors have the body `{ ok: false, error }`.
225
251
 
226
252
  ### Request guards
227
253
 
228
- The daemon rejects browser-originated requests (`Origin` header or cross-site `Sec-Fetch-Site`: 403), `Host` names that are neither IP literals nor local/allow-listed names (403, prevents DNS rebinding), non-JSON POST bodies (415) and bodies above 16 MB (413). Tool calls are rate limited per instance and per client address (429 with `Retry-After`). See `SECURITY.md`.
254
+ The host has no authentication, so it keeps web pages out: it rejects browser-originated requests (`Origin` header or cross-site `Sec-Fetch-Site`: 403), `Host` names that are neither IP literals nor `localhost`, `host.docker.internal` or a name in `allowedHosts` (403, prevents DNS rebinding), POST bodies that are not `application/json` (415) and bodies above 16 MB (413).
255
+
256
+ Rate limits are token buckets (`rateLimit` per second, `rateBurst` at once; `rateLimit: 0` disables them), answered with 429 and `Retry-After`:
257
+
258
+ - tool calls per `instanceId`, and, because instance IDs are self-declared, per client address at 5x;
259
+ - hooks per conversation (`<client>:<session_id>`) and per client address at 5x, in buckets of their own, so a busy conversation's hooks do not use up the tool-call budget of its address;
260
+ - `GET /health` and `GET /api/tools` are not limited.
261
+
262
+ The trust model is unchanged: every process of the same operating-system user is trusted. See `SECURITY.md`.
229
263
 
230
264
  ## Scope Boundaries
231
265
 
232
266
  - No cloud bridge by default.
233
- - No cross-machine live sync by default — point `NEXUS_ENDPOINT` at a reachable host for that.
234
- - Single daemon node; no built-in clustering.
267
+ - No cross-machine live sync by default — point `NEXUS_ENDPOINT` at a reachable host for that, or set it to `discover` to find a host with LAN access on through a UDP broadcast.
268
+ - Single host node; no built-in clustering.
235
269
  - No authentication layer in the open-source baseline.