n2n-nexus 0.9.3 → 0.9.4

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.
@@ -8,12 +8,12 @@ Parameter tables below are derived from `contract/tools.json`, the JSON Schema t
8
8
 
9
9
  ## Conventions
10
10
 
11
- - Every successful result is returned twice: as `structuredContent` (a JSON object) and as the same JSON in a text block for clients that only read text. Tools that return data (`search_projects`, `get_global_topology`, `get_project`, `read_messages`, `list_events`, `get_global_strategy`, `list_global_docs`, `get_global_doc`, `list_meetings`, `get_task`, `list_tasks`, `peers`, `read`, `set_profile`) also publish an `outputSchema`; acknowledgement-only tools do not publish one, to keep `tools/list` small. The result shapes are the `outputSchema` entries in `contract/tools.json`; the host validates every result against them.
12
- - Calls are rate limited per instance (default 20 per second, burst 60; `rateLimit` and `rateBurst` in the host's `settings.json`). Over the limit a call fails with "Rate limit exceeded ... retry after Ns"; retry after that delay.
11
+ - Every successful result is returned twice: as `structuredContent` (a JSON object) and as the same JSON in a text block for clients that only read text. Tools that return data (`search_projects`, `get_global_topology`, `get_project`, `read_messages`, `list_events`, `get_global_strategy`, `list_global_docs`, `get_global_doc`, `list_meetings`, `stance`, `get_task`, `list_tasks`, `peers`, `read`, `set_profile`) also publish an `outputSchema`; acknowledgement-only tools do not publish one, to keep `tools/list` small. The result shapes are the `outputSchema` entries in `contract/tools.json`; the host validates every result against them.
12
+ - Calls are rate limited per conversation, per instance without the hooks (default 20 per second, burst 60; `rateLimit` and `rateBurst` in the host's `settings.json`). Over the limit a call fails with "Rate limit exceeded ... retry after Ns"; retry after that delay.
13
13
  - Every tool has a display `title` (for example "Delete Project") for client UIs.
14
- - Every tool carries MCP annotations: `readOnlyHint`, `destructiveHint`, `idempotentHint` and `openWorldHint: false` (all tools act only on the host). Per the MCP definition a tool is non-destructive only when its updates are purely additive. Read tools are marked read-only. `start_meeting`, `create_task`, `send_message`, `send` and `reply` only add data and are non-destructive; `update_task` is marked non-destructive and idempotent too. `listen`, `set_profile`, `sync_project_assets`, `update_project`, `upload_project_asset`, `rename_project`, `sync_global_doc`, `update_global_strategy`, `end_meeting`, `archive_meeting`, `reopen_meeting`, `cancel_task`, `host_maintenance` and `host_delete_project` rewrite or remove existing state and are marked destructive, so clients can ask for confirmation. `read_messages` and `read` are not read-only because they advance the caller's own unread state, but they are non-destructive since no shared data changes.
15
- - Every adapter has an instance ID (`NEXUS_INSTANCE_ID`, `--id`, or `mcp-<hostname>-<hash of the working directory>-<IDE name>` by default, where the IDE name is the MCP client name from the request's `io.modelcontextprotocol/clientInfo` or, for 2025-era clients, from `initialize`). Read cursors are keyed on it, so it stays the same across restarts of the same IDE in the same workspace while two IDEs on one workspace stay distinct. The adapter sends it as `instanceId` with every call; `read_messages` fails without one.
16
- - Two kinds of id appear in results. The direct-message tools (`peers`, `send`, `read`, `reply`) use conversation session ids, `claude:<id>` or `codex:<id>`. `from` in `read_messages`, `actor` in `list_events`, a meeting's `initiator` and `participants`, and `updatedBy` of global docs are adapter instance ids (`mcp-…`); they are not sessions and cannot be used with `send`.
14
+ - Every tool carries MCP annotations: `readOnlyHint`, `destructiveHint`, `idempotentHint` and `openWorldHint: false` (all tools act only on the host). Per the MCP definition a tool is non-destructive only when its updates are purely additive. Read tools are marked read-only. `start_meeting`, `join_meeting`, `create_task`, `send_message`, `send` and `reply` only add data and are non-destructive (`join_meeting` is also idempotent); `update_task` is marked non-destructive and idempotent too. `listen`, `set_profile`, `sync_project_assets`, `update_project`, `upload_project_asset`, `rename_project`, `sync_global_doc`, `update_global_strategy`, `end_meeting`, `archive_meeting`, `reopen_meeting`, `leave_meeting`, `stance`, `cancel_task`, `host_maintenance` and `host_delete_project` rewrite or remove existing state and are marked destructive, so clients can ask for confirmation. `read_messages` and `read` are not read-only because they advance the caller's own unread state, but they are non-destructive since no shared data changes.
15
+ - Every adapter has an instance ID (`NEXUS_INSTANCE_ID`, `--id`, or `mcp-<hostname>-<hash of the working directory>-<IDE name>` by default, where the IDE name is the MCP client name from the request's `io.modelcontextprotocol/clientInfo` or, for 2025-era clients, from `initialize`). It stays the same across restarts of the same IDE in the same workspace while two IDEs on one workspace stay distinct. The adapter sends it as `instanceId` with every call. Several conversations of one IDE in one workspace share it, so chat and meeting tools identify the conversation instead when the n2n-nexus hooks are installed (see the next item); a client without the hooks is identified by its instance, and `read_messages` then fails without `instanceId`.
16
+ - Two kinds of id appear in results: conversation session ids, `claude:<id>` or `codex:<id>`, and adapter instance ids (`mcp-…`). The direct-message tools (`peers`, `send`, `read`, `reply`) use session ids. Chat and meetings use the calling conversation's session id when the hooks are installed, else its instance id: the sender and `from` of `send_message` / `read_messages`, the unread cursor, a meeting's `initiator` and `participants` and the `actor` of `meeting.*` events; results say which with `identity` (`session` or `instance`). A session id there can be used with `send`; an instance id cannot. If an instance has had a conversation identified within the last 30 minutes and one of its calls cannot be identified, the call fails with a retry message instead of using the instance's shared cursor. Other events and `updatedBy` of global docs keep the instance id.
17
17
  - Project IDs follow `[prefix]_[name]`; the prefixes are listed in the [Assistant guide](ASSISTANT_GUIDE.md#2-project-ids).
18
18
  - All project write tools run synchronously and return their result directly. The task tools (`create_task`, `get_task`, `list_tasks`, `update_task`, `cancel_task`) track work that assistants manage themselves.
19
19
  - Project tools take `projectId` explicitly.
@@ -150,25 +150,25 @@ These six tools are direct messages between conversations. Global chat and meeti
150
150
  | Tool | Parameters | Result and behavior |
151
151
  | --- | --- | --- |
152
152
  | `peers` | `includeOffline?: boolean = false` | `{ self: Profile, peers: Profile[] }`; the other sessions that are not offline, most recent first. With `includeOffline`, also the offline ones active in the last 24 hours. |
153
- | `send` | `to: string`, `text: string` (1–32000 chars), `notifyWhenIdle?: boolean = false` | `{ messageId, to: { id, name, status } }`. `to` is a session ID (any status) or the name of a session that is not offline; you cannot message yourself. Offline recipients get the message when they resume. With `notifyWhenIdle`, you get one notice when the recipient next ends its turn, starts listening or goes offline (the subscription lapses after 24 hours). |
154
- | `read` | `count?: integer = 10` (1–50) | `{ messages: [{ messageId, from: { id, name }, replyTo, text, createdAt }], remaining }`; unread messages oldest first, marked read. |
155
- | `reply` | `messageId: string`, `text: string` (1–32000 chars) | Same result as `send`. The message must have been sent to you; the reply goes to its sender with `replyTo` set to it. |
153
+ | `send` | `to: string`, `text: string` (1–32000 chars), `notifyWhenIdle?: boolean = false`, `expectsReply?: boolean = true`, `wake?: boolean = true` | `{ messageId, to: { id, name, status } }`. `to` is a session ID (any status) or the name of a session that is not offline; you cannot message yourself. Offline recipients get the message when they resume. With `notifyWhenIdle`, you get one notice when the recipient next ends its turn, starts listening or goes offline (the subscription lapses after 24 hours). `expectsReply: false` tells the recipient no reply is needed; `wake: false` never wakes the recipient or holds its turn: it sees the message at its next prompt or tool call. A plain acknowledgement sets both to false, or is not sent. |
154
+ | `read` | `count?: integer = 10` (1–50) | `{ messages: [{ messageId, from: { id, name }, replyTo, text, createdAt, expectsReply }], remaining }`; unread messages oldest first, marked read. |
155
+ | `reply` | `messageId: string`, `text: string` (1–32000 chars), `expectsReply?: boolean = true`, `wake?: boolean = true` | Same result as `send`. The message must have been sent to you; the reply goes to its sender with `replyTo` set to it. |
156
156
  | `listen` | `on: boolean` | `{ listening }`. While on, each time the conversation ends a turn its Stop hook waits up to 590 seconds and continues the conversation when a message or notice arrives. In Claude Code, pressing Esc ends one wait. |
157
- | `set_profile` | `name?: string` (1–48 of `A–Z a–z 0–9 . _ @ # -`), `note?: string` (≤ 200 chars); at least one | `{ profile }`. Names are unique among sessions that are not offline. |
157
+ | `set_profile` | `name?: string` (1–48 of `A–Z a–z 0–9 . _ @ # -`), `note?: string` (≤ 200 chars), `title?: string` (≤ 80 chars); at least one | `{ profile }`. Names are unique among sessions that are not offline. A `title` you set replaces the one from your client until you set it to `""`. |
158
158
 
159
- `Profile` contains `id`, `name`, `client` (`claude` or `codex`), `cwd`, `note`, `status` and `lastActiveAt`. A session is first named `<client>@<working directory name>`, with `#2`, `#3`, … when that name is taken by a session that is not offline. `status` is `working` (the user sent a prompt or a tool ran), `idle` (the turn ended; a session with a background wait for messages shows as `listening` instead), `listening` (waiting in listen mode) or `offline` (the conversation ended). Clients do not always report the end of a conversation, so an `idle` or `working` session without any activity for 30 minutes becomes `offline` too; its next event brings it back. Only a `listening` session answers right away; `idle` and `working` ones see a message when their user next sends a prompt or they next call a tool.
159
+ `Profile` contains `id`, `name`, `title`, `client` (`claude` or `codex`), `cwd`, `note`, `status` and `lastActiveAt`. `title` is the conversation's title as its client shows it (Claude Code: the conversation's title or the name you gave it; Codex: the thread's name, else its first prompt), read by the hooks when a conversation starts, at each prompt and when a turn ends, up to 80 characters; `""` until there is one. It is shown next to the name in `peers`, introductions, notifications and meeting members; `name` stays the handle for `send`. A session is first named `<client>@<working directory name>`, with `#2`, `#3`, … when that name is taken by a session that is not offline. `status` is `working` (the user sent a prompt or a tool ran), `idle` (the turn ended; a session with a background wait for messages shows as `listening` instead), `listening` (waiting in listen mode) or `offline` (the conversation ended). Clients do not always report the end of a conversation, so an `idle` or `working` session without any activity for 30 minutes becomes `offline` too; its next event brings it back. Only a `listening` session answers right away; `idle` and `working` ones see a message when their user next sends a prompt or they next call a tool.
160
160
 
161
- Delivery. The recipient's hooks announce new messages; notifications carry only the sender, the time and the first line (up to 100 characters), and the text arrives through `read`, as tool output:
161
+ Delivery. The recipient's hooks announce new messages; notifications carry only the sender, the time and the first line (up to 100 characters), and the text arrives through `read`, as tool output. Each message is listed once; while it stays unread, later notifications only count it. A message with `expectsReply: false` is marked `(no reply needed)`, and a notification whose messages all need no reply does not ask for one. News of meetings the session has joined arrives the same way, one line per meeting (see [`join_meeting`](#join_meeting)):
162
162
 
163
163
  | Client event | What the recipient sees |
164
164
  | --- | --- |
165
165
  | Conversation started | Its own name and ID, the other sessions that are not offline (up to five listed), which of them can answer right away, and how to use `peers`, `send` and `read`. |
166
- | Prompt submitted | Count of unread messages, up to five listed, and pending notices. |
167
- | After each tool call | Only messages and notices not announced yet. |
168
- | Turn about to end | If anything is unread, the turn continues once with the announcement. |
169
- | Listening | The wait ends as soon as something new arrives, and the conversation continues with it. |
170
- | Idle (turn ended) | A background wait started by the Stop hook (`n2n-nexus hook <client> --wait`, up to an hour) starts a new turn with the announcement: Claude Code through an `asyncRewake` hook (exit code 2), Codex through `codex queue --thread <session id>` (`NEXUS_CODEX_COMMAND` overrides the `codex` command). In an unattended Claude session (`claude -p`, `CLAUDE_CODE_SESSION_ATTENDED=0`) the hook returns at once, since `claude -p` waits for it before exiting. Meanwhile the session shows as `listening`; any new activity in the conversation ends the wait. Codex starts the queued turn only for a conversation loaded in the running Codex app, and the wait ends when Codex quits. |
171
- | Automatic turn limit | Waking, a listening Stop and a Stop held open for messages each count as an automatic turn; the announcement says `Automatic turn n of N`. With `maxAutoTurns` set (host `settings.json`; default 0, no limit, and no count in the announcement), after that many of them since the user's last prompt, messages are still stored but no longer wake or hold the conversation; the user's next prompt announces them and starts counting again. A prompt within a minute of a wake is the woken turn starting and does not reset the count. |
166
+ | Prompt submitted | New messages (up to five listed), the count of unread ones announced before, pending notices and meeting news. The prompt that starts a woken turn adds only what arrived after the wake. |
167
+ | After each tool call | Only messages, notices and meeting news not announced yet. |
168
+ | Turn about to end | If an unread message may wake the session (`wake` true), a notice is pending or a meeting the session takes part in actively has news, the turn continues once with the announcement. |
169
+ | Listening | The wait ends as soon as something arrives that may wake the session, and the conversation continues with it. |
170
+ | Idle (turn ended) | A background wait started by the Stop hook (`n2n-nexus hook <client> --wait`, up to an hour) starts a new turn with the announcement: Claude Code through an `asyncRewake` hook (exit code 2), Codex through `codex queue --thread <session id>` (`NEXUS_CODEX_COMMAND` overrides the `codex` command). In an unattended Claude session (`claude -p`, `CLAUDE_CODE_SESSION_ATTENDED=0`) the hook returns at once, since `claude -p` waits for it before exiting. Meanwhile the session shows as `listening`; any new activity in the conversation ends the wait. Codex starts the queued turn only for a conversation loaded in the running Codex app, and the wait ends when Codex quits; when `codex queue` fails, the wake is undone and the wait delivers the same messages again after a delay (1 s, doubling up to 5 minutes). |
171
+ | Automatic turn limit | Waking, a listening Stop and a Stop held open for direct messages or their notices each count as an automatic turn; meeting news and meeting notices do not count and are not limited; the announcement says `Automatic turn n of N`. With `maxAutoTurns` set (host `settings.json`; default 0, no limit, and no count in the announcement), after that many of them since the user's last prompt, messages are still stored but no longer wake or hold the conversation; the user's next prompt announces them and starts counting again. A prompt within a minute of a wake is the woken turn starting and does not reset the count. |
172
172
 
173
173
  Unread messages expire after 24 hours; the sender then gets a notice. Notices also report `notifyWhenIdle` results.
174
174
 
@@ -183,6 +183,7 @@ Post to a meeting or to global chat. For a message to one conversation, use `sen
183
183
  | `message` | string | yes | Message body. |
184
184
  | `category` | string | no | One of `PROPOSAL`, `DECISION`, `UPDATE`, `CHAT` or `MEETING_START` (reserved; the host never writes it). Omitted, the message has no category. |
185
185
  | `meetingId` | string | no | Target meeting. Must be active. |
186
+ | `draft` | boolean | no | Publish the message as the meeting's current draft (meetings only; the category is `PROPOSAL`). See [`stance`](#stance). |
186
187
 
187
188
  Behavior:
188
189
 
@@ -190,23 +191,27 @@ Behavior:
190
191
  - Otherwise, if a meeting is active, the message goes to it.
191
192
  - Otherwise it is written to global chat.
192
193
  - A `DECISION` message in a meeting is also recorded in that meeting's decisions (`decisionCount` in `list_meetings`).
193
- - The sender stored with the message is the calling adapter instance ID.
194
- - Result: `{ ok: true, sentTo: "meeting" | "global", meetingId?, messageId?, message }`; global chat message IDs are numbers.
194
+ - The sender stored with the message is the calling conversation's session ID, or the adapter instance ID without hooks.
195
+ - Result: `{ ok: true, sentTo: "meeting" | "global", meetingId?, messageId, identity, currentProposalId?, message }` (`currentProposalId` with `draft`); `messageId` is the message's number in its meeting, counted from 1 in each meeting, or its global chat id (use it with `afterId`).
195
196
 
196
197
  ### `read_messages`
197
198
 
198
- Read unread global chat or meeting messages. Direct messages sent to your conversation are read with `read`.
199
+ Read global chat or meeting messages: the unread ones (default) or history. Direct messages sent to your conversation are read with `read`.
199
200
 
200
201
  | Parameter | Type | Required | Default | Notes |
201
202
  | --- | --- | --- | --- | --- |
202
203
  | `count` | integer | no | `10` | Number of messages to read (1–500). |
203
- | `meetingId` | string | no | | When provided, reads from that meeting. Otherwise reads from global logs. |
204
+ | `meetingId` | string | no | | When provided, reads from that meeting. Otherwise reads from global chat. |
205
+ | `latest` | boolean | no | | History mode: the newest `count` messages, oldest first. Not with `afterId`. |
206
+ | `afterId` | integer | no | | History mode: the first `count` messages with an id above `afterId` (in a meeting, its message number). Not with `latest`. |
204
207
 
205
208
  Behavior:
206
209
 
207
- - Both global and meeting reads are incremental per adapter instance: each call returns only messages not yet returned to that instance, oldest first, and advances the cursor.
208
- - Each message's `from` is the sending adapter instance ID, not a session ID: you cannot `send` to it.
209
- - Needs the calling instance: a call without `instanceId` fails with `read_messages needs the instanceId field of the POST /api/tools/call body (the n2n-nexus MCP adapter sends it automatically); unread cursors are kept per instance.` The MCP adapter always sends one.
210
+ - Unread mode is incremental per conversation (per adapter instance without hooks): each call returns messages of others not yet returned to it, oldest first, and moves its cursor past them and past its own messages.
211
+ - A conversation's first unread read of global chat starts at the newest message: it returns nothing and a `note`; read earlier messages with `latest` or `afterId`. Its first read of a meeting starts at the meeting's first message.
212
+ - History mode moves no cursor, includes your own messages and needs no identity.
213
+ - Result: `{ source: "global" | "meeting", mode: "unread" | "history", identity?, meetingId?, messages: [{ id, from, senderName, text, category, timestamp }], count, note? }`. `from` is the sender's session ID (`senderName` is its name, usable with `send`) or instance ID (`senderName` null); `category` is null for a message without one. A meeting message's `id` is its number in the meeting, from 1; global chat messages have their own ids.
214
+ - Unread mode without hooks needs the calling instance: a call without `instanceId` fails with `read_messages needs the instanceId field of the POST /api/tools/call body (the n2n-nexus MCP adapter sends it automatically); unread cursors are kept per instance.` The MCP adapter always sends one.
210
215
 
211
216
  ### `list_events`
212
217
 
@@ -219,8 +224,8 @@ Read the structured activity log.
219
224
 
220
225
  Behavior:
221
226
 
222
- - Every state change made through the host's tools is recorded: `project.sync`, `project.update`, `project.rename`, `project.delete`, `project.asset`, `doc.sync`, `strategy.update`, `meeting.start`, `meeting.end`, `meeting.archive`, `meeting.reopen`, `maintenance.prune`, `maintenance.clear`.
223
- - Each event carries `timestamp`, `actor` (adapter instance ID, not a session ID), `action`, optional `target` and `detail`.
227
+ - Every state change made through the host's tools is recorded: `project.sync`, `project.update`, `project.rename`, `project.delete`, `project.asset`, `doc.sync`, `strategy.update`, `meeting.start`, `meeting.end`, `meeting.archive`, `meeting.reopen`, `meeting.join`, `meeting.leave`, `maintenance.prune`, `maintenance.clear`.
228
+ - Each event carries `timestamp`, `actor`, `action`, optional `target` and `detail`. `actor` of `meeting.*` events is the calling conversation's session ID (its adapter instance ID without hooks); other events record the adapter instance ID.
224
229
 
225
230
  ### `update_global_strategy`
226
231
 
@@ -286,7 +291,9 @@ List meetings, newest first.
286
291
 
287
292
  Behavior:
288
293
 
289
- - Returns `{ count, activeMeetingId, meetings: [{ id, topic, status, initiator, startTime, endTime?, participants, messageCount, decisionCount, summary? }] }`.
294
+ - Returns `{ count, activeMeetingId, meetings: [{ id, topic, status, initiator, startTime, endTime?, participants, members, messageCount, decisionCount, wakes, summary?, endReason, proposal }] }`.
295
+ - `endReason` is `consensus` (closed by [`stance`](#stance)), `manual` (`end_meeting`) or null. `proposal` is the current draft, or null: `{ messageId, author, approversVersion, required, endorsed, waiting, objections: [{ id, note }] }`.
296
+ - `participants` is everyone who has posted; `members` is who has joined and not left: `[{ id, name, title, mode, approver, status, joinedAt, wakes, lastWokenAt }]`, where `name` and `status` are the session's (null for an adapter instance). `wakes` counts the wakes and held Stops the meeting caused, per member and for the meeting; it is only shown, nothing ends a meeting because of it.
290
297
  - Read a meeting's messages with `read_messages(meetingId)`.
291
298
 
292
299
  ### `start_meeting`
@@ -299,7 +306,7 @@ Open a new meeting session.
299
306
 
300
307
  Behavior:
301
308
 
302
- - The calling adapter instance becomes the meeting's `initiator`.
309
+ - The calling conversation (its adapter instance without hooks) becomes the meeting's `initiator` and joins it as an `active`, `required` member.
303
310
  - The new meeting becomes the default meeting for everyone: `send_message` and `end_meeting` without `meetingId` use it until it ends (then the most recently activated meeting still active, if any). `reopen_meeting` makes the reopened meeting the default the same way.
304
311
 
305
312
  ### `end_meeting`
@@ -330,6 +337,58 @@ Reactivate a closed or archived meeting.
330
337
  Behavior:
331
338
 
332
339
  - Any adapter instance can end, archive or reopen a meeting, not only its `initiator`.
340
+ - `end_meeting` closes with `endReason: manual`, not a consensus; `reopen_meeting` clears it and the current draft: a reopened meeting decides on a new draft (earlier stances stay as history).
341
+ - Ending a meeting gives each member but the one who ended it one notice that it ended (with the summary), which needs no reply; active members are woken with it, async members see it at their next prompt or tool call. Reopening does not bring back members that left.
342
+
343
+ ### `join_meeting`
344
+
345
+ Join an active meeting, or change how you take part in it.
346
+
347
+ | Parameter | Type | Required | Default | Notes |
348
+ | --- | --- | --- | --- | --- |
349
+ | `meetingId` | string | yes | | Meeting to join. Must be active. |
350
+ | `mode` | string | no | `active` | `active`: new messages of others wake you (or hold your turn's end); `async`: you see them at your next prompt or tool call. |
351
+ | `approver` | string | no | `required` when active, `optional` when async | Whether your approval is required for a decision. |
352
+ | `since` | `beginning`, `latest` or integer | no | `latest` on the first join | Where `read_messages` starts for this meeting: from its first message, after its newest, or after that message number (`0`: from the beginning). A later join without `since` keeps your cursor. |
353
+
354
+ Behavior:
355
+
356
+ - Result: `{ ok, meetingId, mode, approver, identity, delivery }`. `delivery` is `wake` (active), `next-hook` (async) or `unavailable`: a client without the hooks joins as its adapter instance, which nothing can notify.
357
+ - News is announced through the hooks as one line per meeting, `Meeting "<topic>" (<id>): N new message(s) (#a–#b) from <senders>.`, followed by how to read them; the text is read with `read_messages(meetingId)`. Your own messages never wake you. News of a meeting does not count as an automatic turn and still arrives past `maxAutoTurns`; a meeting runs until it is ended.
358
+ - Messages from before you joined are not announced. Joining again while a member only changes `mode` and `approver`; switch to `async` to pause. After `leave_meeting`, joining again starts from the newest message.
359
+ - Records a `meeting.join` event.
360
+
361
+ ### `leave_meeting`
362
+
363
+ Leave a meeting: you are no longer notified about it and your approval no longer counts.
364
+
365
+ | Parameter | Type | Required | Notes |
366
+ | --- | --- | --- | --- |
367
+ | `meetingId` | string | yes | Meeting to leave. Fails when you are not a member. |
368
+
369
+ Behavior:
370
+
371
+ - Result: `{ ok, meetingId }`. Records a `meeting.leave` event. `join_meeting` joins again; `reopen_meeting` does not.
372
+ - Leaving removes you from the current draft's required approvers (see [`stance`](#stance)).
373
+
374
+ ### `stance`
375
+
376
+ Endorse or object to a proposal, or withdraw your stance on it. A meeting reaches a decision by consensus on its current draft.
377
+
378
+ | Parameter | Type | Required | Notes |
379
+ | --- | --- | --- | --- |
380
+ | `meetingId` | string | yes | An active meeting. |
381
+ | `messageId` | integer | yes | The proposal's number in the meeting (a `PROPOSAL` message). |
382
+ | `stance` | string | yes | `endorse`, `object` or `withdraw`. |
383
+ | `note` | string | for `object` | The reason of an objection, or a reservation with an endorsement. |
384
+
385
+ Behavior:
386
+
387
+ - The current draft is the message last published with `send_message(..., draft: true)`; an ordinary `PROPOSAL` changes neither the draft nor anyone's stance. Publishing a draft fixes its required approvers: the members joined as `required` through the hooks (adapter instances cannot approve). The list changes only when one of them leaves or changes `approver` with `join_meeting` (a member that joined before the draft can be added back as `required`; members joining later are not added); each change is posted to the meeting with the new `approversVersion`.
388
+ - You have one stance per proposal; a new one replaces it and `withdraw` removes it. An objection is also posted to the meeting with its reason. The proposal's author gets a notice of every stance; nobody else is woken by an endorsement.
389
+ - The meeting closes by consensus when the current draft has at least 2 required approvers, every one of them endorsed it and nobody objects to it (an adapter instance's objection blocks too): a `DECISION` with the draft's text is posted by `n2n-nexus`, the meeting closes with `endReason: consensus` and the other members get the closing notice, which names the draft and the decision's message number. With fewer than 2 required approvers it closes only with `end_meeting`.
390
+ - Result: `{ ok, meetingId, messageId, stance, currentProposalId, counted, approvals: { required, endorsed, waiting, objections: [{ id, note }] }, decided }`. `counted` is false for an adapter instance; `waiting` are required approvers without any stance; `decided` is true when this stance closed the meeting.
391
+ - When a meeting with a draft stays quiet for `stanceReminderMinutes` (host `settings.json`, default 10) and required approvers have not taken a stance, each of them is reminded once and the author is told who is missing. Reminders never end a meeting.
333
392
 
334
393
  ## Task management
335
394
 
@@ -8,12 +8,12 @@
8
8
 
9
9
  ## 约定
10
10
 
11
- - 每个成功的结果都返回两份:一份是 `structuredContent`(JSON 对象),另一份是放在文本块里的同一段 JSON,供只读取文本的客户端使用。返回数据的工具(`search_projects`、`get_global_topology`、`get_project`、`read_messages`、`list_events`、`get_global_strategy`、`list_global_docs`、`get_global_doc`、`list_meetings`、`get_task`、`list_tasks`、`peers`、`read`、`set_profile`)还会提供 `outputSchema`;只返回确认的工具不提供,以保持 `tools/list` 精简。结果的结构就是 `contract/tools.json` 里的 `outputSchema` 条目;host 用它们校验每一个结果。
12
- - 调用按实例限流(默认每秒 20 次,突发 60 次;对应 host `settings.json` 中的 `rateLimit` 和 `rateBurst`)。超出限制的调用会失败并返回 "Rate limit exceeded ... retry after Ns";等待该时长后重试。
11
+ - 每个成功的结果都返回两份:一份是 `structuredContent`(JSON 对象),另一份是放在文本块里的同一段 JSON,供只读取文本的客户端使用。返回数据的工具(`search_projects`、`get_global_topology`、`get_project`、`read_messages`、`list_events`、`get_global_strategy`、`list_global_docs`、`get_global_doc`、`list_meetings`、`stance`、`get_task`、`list_tasks`、`peers`、`read`、`set_profile`)还会提供 `outputSchema`;只返回确认的工具不提供,以保持 `tools/list` 精简。结果的结构就是 `contract/tools.json` 里的 `outputSchema` 条目;host 用它们校验每一个结果。
12
+ - 调用按对话限流,没装 hooks 时按实例(默认每秒 20 次,突发 60 次;对应 host `settings.json` 中的 `rateLimit` 和 `rateBurst`)。超出限制的调用会失败并返回 "Rate limit exceeded ... retry after Ns";等待该时长后重试。
13
13
  - 每个工具都有供客户端界面显示的 `title`(例如 "Delete Project")。
14
- - 每个工具都带有 MCP annotations:`readOnlyHint`、`destructiveHint`、`idempotentHint` 和 `openWorldHint: false`(所有工具都只作用于 host)。按照 MCP 的定义,只有更新完全是追加时,工具才算非破坏性。读取类工具标为只读。`start_meeting`、`create_task`、`send_message`、`send` 和 `reply` 只追加数据,是非破坏性的;`update_task` 也标为非破坏性且幂等。`listen`、`set_profile`、`sync_project_assets`、`update_project`、`upload_project_asset`、`rename_project`、`sync_global_doc`、`update_global_strategy`、`end_meeting`、`archive_meeting`、`reopen_meeting`、`cancel_task`、`host_maintenance` 和 `host_delete_project` 会改写或删除已有状态,标为破坏性,以便客户端请求确认。`read_messages` 和 `read` 不是只读的,因为它们会推进调用方自己的未读状态;但它们是非破坏性的,因为不改变任何共享数据。
15
- - 每个适配器都有一个实例 ID(`NEXUS_INSTANCE_ID`、`--id`,默认是 `mcp-<hostname>-<工作目录的哈希>-<IDE 名>`,其中 IDE 名是 MCP 客户端名,取自请求的 `io.modelcontextprotocol/clientInfo`,2025 代的客户端则取自 `initialize`)。读取游标以它为键,所以同一个 IDE 在同一个工作区里重启后它保持不变,而同一个工作区里的两个 IDE 仍然互相区分。适配器在每次调用中都以 `instanceId` 带上它;没有它时 `read_messages` 会失败。
16
- - 结果里会出现两种 ID。直接消息工具(`peers`、`send`、`read`、`reply`)使用对话的会话 ID,即 `claude:<id>` 或 `codex:<id>`。`read_messages` 中的 `from`、`list_events` 中的 `actor`、会议的 `initiator` 和 `participants`,以及全局文档的 `updatedBy` 都是适配器实例 ID(`mcp-…`);它们不是会话,不能用于 `send`。
14
+ - 每个工具都带有 MCP annotations:`readOnlyHint`、`destructiveHint`、`idempotentHint` 和 `openWorldHint: false`(所有工具都只作用于 host)。按照 MCP 的定义,只有更新完全是追加时,工具才算非破坏性。读取类工具标为只读。`start_meeting`、`join_meeting`、`create_task`、`send_message`、`send` 和 `reply` 只追加数据,是非破坏性的(`join_meeting` 也是幂等的);`update_task` 也标为非破坏性且幂等。`listen`、`set_profile`、`sync_project_assets`、`update_project`、`upload_project_asset`、`rename_project`、`sync_global_doc`、`update_global_strategy`、`end_meeting`、`archive_meeting`、`reopen_meeting`、`leave_meeting`、`stance`、`cancel_task`、`host_maintenance` 和 `host_delete_project` 会改写或删除已有状态,标为破坏性,以便客户端请求确认。`read_messages` 和 `read` 不是只读的,因为它们会推进调用方自己的未读状态;但它们是非破坏性的,因为不改变任何共享数据。
15
+ - 每个适配器都有一个实例 ID(`NEXUS_INSTANCE_ID`、`--id`,默认是 `mcp-<hostname>-<工作目录的哈希>-<IDE 名>`,其中 IDE 名是 MCP 客户端名,取自请求的 `io.modelcontextprotocol/clientInfo`,2025 代的客户端则取自 `initialize`)。同一个 IDE 在同一个工作区里重启后它保持不变,而同一个工作区里的两个 IDE 仍然互相区分。适配器在每次调用中都以 `instanceId` 带上它。同一个 IDE 在同一个工作区里的多个对话共用它,所以装了 n2n-nexus hooks 时,聊天和会议工具改为识别对话本身(见下一条);没装 hooks 的客户端按实例识别,这时没有 `instanceId` 的 `read_messages` 会失败。
16
+ - 结果里会出现两种 ID:对话的会话 ID(`claude:<id>` 或 `codex:<id>`)和适配器实例 ID(`mcp-…`)。直接消息工具(`peers`、`send`、`read`、`reply`)使用会话 ID。聊天和会议在装了 hooks 时使用调用方对话的会话 ID,否则使用实例 ID:`send_message` / `read_messages` 的发送方和 `from`、未读游标、会议的 `initiator` 和 `participants`、`meeting.*` 事件的 `actor`;结果里的 `identity`(`session` 或 `instance`)说明是哪一种。这里的会话 ID 可以用于 `send`,实例 ID 不能。某个实例在最近 30 分钟内识别出过对话、而这次调用识别不出时,调用会失败并提示重试,而不会改用该实例共用的游标。其他事件和全局文档的 `updatedBy` 仍是实例 ID。
17
17
  - 项目 ID 采用 `[prefix]_[name]` 格式;前缀列在[助手指南](ASSISTANT_GUIDE_zh.md#2-项目-id)中。
18
18
  - 所有项目写入工具都同步执行并直接返回结果。任务工具(`create_task`、`get_task`、`list_tasks`、`update_task`、`cancel_task`)用于跟踪由助手自己管理的工作。
19
19
  - 项目工具显式接收 `projectId`。
@@ -150,25 +150,25 @@
150
150
  | 工具 | 参数 | 结果与行为 |
151
151
  | --- | --- | --- |
152
152
  | `peers` | `includeOffline?: boolean = false` | `{ self: Profile, peers: Profile[] }`;其他未离线的会话,最近活跃的在前。带 `includeOffline` 时,还包括过去 24 小时内活跃过的离线会话。 |
153
- | `send` | `to: string`、`text: string`(1–32000 个字符)、`notifyWhenIdle?: boolean = false` | `{ messageId, to: { id, name, status } }`。`to` 是会话 ID(任何状态)或某个未离线会话的名称;不能给自己发消息。离线的收件方在恢复时收到消息。带 `notifyWhenIdle` 时,收件方下一次结束一轮、开始待命或离线时,你会收到一次通知(订阅 24 小时后失效)。 |
154
- | `read` | `count?: integer = 10`(1–50) | `{ messages: [{ messageId, from: { id, name }, replyTo, text, createdAt }], remaining }`;未读消息,最早的在前,读取后标为已读。 |
155
- | `reply` | `messageId: string`、`text: string`(1–32000 个字符) | 结果与 `send` 相同。该消息必须是发给你的;回复发给它的发送方,`replyTo` 设为该消息。 |
153
+ | `send` | `to: string`、`text: string`(1–32000 个字符)、`notifyWhenIdle?: boolean = false`、`expectsReply?: boolean = true`、`wake?: boolean = true` | `{ messageId, to: { id, name, status } }`。`to` 是会话 ID(任何状态)或某个未离线会话的名称;不能给自己发消息。离线的收件方在恢复时收到消息。带 `notifyWhenIdle` 时,收件方下一次结束一轮、开始待命或离线时,你会收到一次通知(订阅 24 小时后失效)。`expectsReply: false` 告诉收件方无需回复;`wake: false` 不叫醒收件方、也不拦住它结束这一轮,它在下次输入或调用工具时看到这条消息。单纯的确认收到要两个都设为 false,或者干脆不发。 |
154
+ | `read` | `count?: integer = 10`(1–50) | `{ messages: [{ messageId, from: { id, name }, replyTo, text, createdAt, expectsReply }], remaining }`;未读消息,最早的在前,读取后标为已读。 |
155
+ | `reply` | `messageId: string`、`text: string`(1–32000 个字符)、`expectsReply?: boolean = true`、`wake?: boolean = true` | 结果与 `send` 相同。该消息必须是发给你的;回复发给它的发送方,`replyTo` 设为该消息。 |
156
156
  | `listen` | `on: boolean` | `{ listening }`。开启期间,对话每结束一轮,其 Stop hook 会等待最多 590 秒,有消息或通知到达时让对话继续。在 Claude Code 中按 Esc 会结束一次等待。 |
157
- | `set_profile` | `name?: string`(1–48 个 `A–Z a–z 0–9 . _ @ # -` 中的字符)、`note?: string`(≤ 200 个字符);至少提供一个 | `{ profile }`。名称在未离线的会话中唯一。 |
157
+ | `set_profile` | `name?: string`(1–48 个 `A–Z a–z 0–9 . _ @ # -` 中的字符)、`note?: string`(≤ 200 个字符)、`title?: string`(≤ 80 个字符);至少提供一个 | `{ profile }`。名称在未离线的会话中唯一。 自己设置的 `title` 会取代客户端提供的标题,直到设为 `""`。 |
158
158
 
159
- `Profile` 包含 `id`、`name`、`client`(`claude` 或 `codex`)、`cwd`、`note`、`status` 和 `lastActiveAt`。会话的初始名称为 `<client>@<工作目录名>`,该名称已被某个未离线会话占用时加上 `#2`、`#3`、…。`status` 为 `working`(用户发送了提示词或有工具在运行)、`idle`(本轮已结束;有后台等待消息的会话改为显示 `listening`)、`listening`(在 listen 模式下等待)或 `offline`(对话已结束)。客户端不一定会报告对话结束,所以 `idle` 或 `working` 的会话 30 分钟没有任何活动也会变为 `offline`;它的下一个事件会让它恢复。只有 `listening` 的会话会立即回应;`idle` 和 `working` 的会话要等它的用户下次发送提示词或它下次调用工具时才会看到消息。
159
+ `Profile` 包含 `id`、`name`、`title`、`client`(`claude` 或 `codex`)、`cwd`、`note`、`status` 和 `lastActiveAt`。`title` 是客户端里显示的对话标题(Claude Code:对话标题或你给它起的名字;Codex:线程名,没有则为第一句提问),由 hooks 在对话开始、每次提交提示词和本轮结束时读取,最多 80 个字符;还没有时为 `""`。它显示在 `peers`、介绍、通知和会议成员里名称的旁边;`send` 仍用 `name`。会话的初始名称为 `<client>@<工作目录名>`,该名称已被某个未离线会话占用时加上 `#2`、`#3`、…。`status` 为 `working`(用户发送了提示词或有工具在运行)、`idle`(本轮已结束;有后台等待消息的会话改为显示 `listening`)、`listening`(在 listen 模式下等待)或 `offline`(对话已结束)。客户端不一定会报告对话结束,所以 `idle` 或 `working` 的会话 30 分钟没有任何活动也会变为 `offline`;它的下一个事件会让它恢复。只有 `listening` 的会话会立即回应;`idle` 和 `working` 的会话要等它的用户下次发送提示词或它下次调用工具时才会看到消息。
160
160
 
161
- 送达。收件方的 hooks 会提醒新消息;通知只带发送方、时间和第一行(最多 100 个字符),正文通过 `read` 以工具输出的形式送达:
161
+ 送达。收件方的 hooks 会提醒新消息;通知只带发送方、时间和第一行(最多 100 个字符),正文通过 `read` 以工具输出的形式送达。每条消息只列出一次;它一直未读时,之后的通知只计数。`expectsReply: false` 的消息会标上 `(no reply needed)`,消息全都无需回复的通知不会要求回复。已加入的会议的新消息也这样送达,每个会议一行(见 [`join_meeting`](#join_meeting)):
162
162
 
163
163
  | 客户端事件 | 收件方看到的内容 |
164
164
  | --- | --- |
165
165
  | 对话开始 | 自己的名称和 ID、未离线的其他会话(最多列出五个)、其中哪些能立即回应,以及 `peers`、`send` 和 `read` 的用法。 |
166
- | 提交提示词 | 未读消息数、最多列出五条,以及待处理的通知。 |
167
- | 每次工具调用后 | 只有尚未提醒过的消息和通知。 |
168
- | 本轮即将结束 | 有未读内容时,本轮带着提醒再继续一次。 |
169
- | 待命中 | 一有新内容到达就结束等待,对话带着它继续。 |
170
- | 空闲(本轮已结束) | Stop hook 启动的后台等待(`n2n-nexus hook <client> --wait`,最长一小时)带着提醒开启新的一轮:Claude Code 通过 `asyncRewake` hook(退出码 2),Codex 通过 `codex queue --thread <会话 ID>`(`NEXUS_CODEX_COMMAND` 可替换 `codex` 命令)。在无人值守的 Claude 会话(`claude -p`、`CLAUDE_CODE_SESSION_ATTENDED=0`)中,该 hook 立即返回,因为 `claude -p` 会等它结束才退出。等待期间会话显示为 `listening`;对话中的任何新活动都会结束等待。Codex 只会为运行中的 Codex 应用里已加载的对话启动排队的一轮,且 Codex 退出时等待结束。 |
171
- | 自动轮次上限 | 被叫醒、待命中的 Stop,以及因消息而不结束的 Stop,各算一次自动轮次;提醒中会写 `Automatic turn n of N`。设置了 `maxAutoTurns` 时(host `settings.json`;默认 0,即不限制,提醒中也不写计数),从用户上次输入以来达到这个次数后,消息仍会保存,但不再叫醒或拦住对话;用户下次输入时提醒这些消息,并重新开始计数。叫醒后一分钟内的提示词视为被叫醒的那一轮开始,不清零计数。 |
166
+ | 提交提示词 | 新消息(最多列出五条)、之前提醒过的未读消息数、待处理的通知和会议新消息。被叫醒的那一轮开始时的提示词只附带叫醒之后新到的内容。 |
167
+ | 每次工具调用后 | 只有尚未提醒过的消息、通知和会议新消息。 |
168
+ | 本轮即将结束 | 有可以叫醒会话的未读消息(`wake` 为 true)、待处理的通知,或会话以 active 方式参加的会议有新消息时,本轮带着提醒再继续一次。 |
169
+ | 待命中 | 一有可以叫醒会话的内容到达就结束等待,对话带着它继续。 |
170
+ | 空闲(本轮已结束) | Stop hook 启动的后台等待(`n2n-nexus hook <client> --wait`,最长一小时)带着提醒开启新的一轮:Claude Code 通过 `asyncRewake` hook(退出码 2),Codex 通过 `codex queue --thread <会话 ID>`(`NEXUS_CODEX_COMMAND` 可替换 `codex` 命令)。在无人值守的 Claude 会话(`claude -p`、`CLAUDE_CODE_SESSION_ATTENDED=0`)中,该 hook 立即返回,因为 `claude -p` 会等它结束才退出。等待期间会话显示为 `listening`;对话中的任何新活动都会结束等待。Codex 只会为运行中的 Codex 应用里已加载的对话启动排队的一轮,且 Codex 退出时等待结束;`codex queue` 失败时这次唤醒会被撤销,等待隔一段时间(1 秒起,每次加倍,最长 5 分钟)后再次送达同样的消息。 |
171
+ | 自动轮次上限 | 被叫醒、待命中的 Stop,以及因私信或其通知而不结束的 Stop,各算一次自动轮次;会议新消息和会议通知不计数,也不受限制;提醒中会写 `Automatic turn n of N`。设置了 `maxAutoTurns` 时(host `settings.json`;默认 0,即不限制,提醒中也不写计数),从用户上次输入以来达到这个次数后,消息仍会保存,但不再叫醒或拦住对话;用户下次输入时提醒这些消息,并重新开始计数。叫醒后一分钟内的提示词视为被叫醒的那一轮开始,不清零计数。 |
172
172
 
173
173
  未读消息 24 小时后过期,发送方随后会收到通知。通知也会报告 `notifyWhenIdle` 的结果。
174
174
 
@@ -183,6 +183,7 @@ HTTP 客户端:hooks 调用 `POST /api/hook`,请求体为 `{ client: "claude
183
183
  | `message` | string | 是 | 消息正文。 |
184
184
  | `category` | string | 否 | `PROPOSAL`、`DECISION`、`UPDATE`、`CHAT` 或 `MEETING_START`(保留值;host 从不写入)之一。不传时消息没有分类。 |
185
185
  | `meetingId` | string | 否 | 目标会议。必须处于活跃状态。 |
186
+ | `draft` | boolean | 否 | 把这条消息发布为会议的当前待决稿(只限会议;类别为 `PROPOSAL`)。见 [`stance`](#stance)。 |
186
187
 
187
188
  行为:
188
189
 
@@ -190,23 +191,27 @@ HTTP 客户端:hooks 调用 `POST /api/hook`,请求体为 `{ client: "claude
190
191
  - 否则,如果有活跃会议,消息发到该会议。
191
192
  - 否则写入全局聊天。
192
193
  - 会议中的 `DECISION` 消息还会记入该会议的决策(`list_meetings` 中的 `decisionCount`)。
193
- - 随消息保存的发送方是调用方的适配器实例 ID。
194
- - 结果:`{ ok: true, sentTo: "meeting" | "global", meetingId?, messageId?, message }`;全局聊天的消息 ID 是数字。
194
+ - 随消息保存的发送方是调用方对话的会话 ID,没装 hooks 时是适配器实例 ID。
195
+ - 结果:`{ ok: true, sentTo: "meeting" | "global", meetingId?, messageId, identity, currentProposalId?, message }`(带 `draft` 时有 `currentProposalId`);`messageId` 是消息在所属会议里的编号(每个会议从 1 开始),或它的全局聊天 ID(可用于 `afterId`)。
195
196
 
196
197
  ### `read_messages`
197
198
 
198
- 读取全局聊天或会议中的未读消息。发给你的对话的直接消息用 `read` 读取。
199
+ 读取全局聊天或会议中的消息:未读消息(默认)或历史。发给你的对话的直接消息用 `read` 读取。
199
200
 
200
201
  | 参数 | 类型 | 必填 | 默认值 | 说明 |
201
202
  | --- | --- | --- | --- | --- |
202
203
  | `count` | integer | 否 | `10` | 要读取的消息数(1–500)。 |
203
- | `meetingId` | string | 否 | | 提供时从该会议读取。否则从全局日志读取。 |
204
+ | `meetingId` | string | 否 | | 提供时从该会议读取。否则从全局聊天读取。 |
205
+ | `latest` | boolean | 否 | | 历史模式:最新的 `count` 条,最早的在前。不能与 `afterId` 同时使用。 |
206
+ | `afterId` | integer | 否 | | 历史模式:ID 大于 `afterId` 的前 `count` 条(会议里是消息编号)。不能与 `latest` 同时使用。 |
204
207
 
205
208
  行为:
206
209
 
207
- - 全局和会议的读取都按适配器实例增量进行:每次调用只返回尚未返回给该实例的消息,最早的在前,并推进游标。
208
- - 每条消息的 `from` 是发送方的适配器实例 ID,不是会话 ID:不能对它使用 `send`。
209
- - 需要调用方实例:没有 `instanceId` 的调用会失败,并返回 `read_messages needs the instanceId field of the POST /api/tools/call body (the n2n-nexus MCP adapter sends it automatically); unread cursors are kept per instance.` MCP 适配器总会带上它。
210
+ - 未读模式按对话增量进行(没装 hooks 时按适配器实例):每次调用返回尚未返回给它的他人消息,最早的在前,游标越过这些消息和它自己发的消息。
211
+ - 对话第一次以未读模式读全局聊天时从最新消息开始:本次不返回消息,带一条 `note`;更早的消息用 `latest` 或 `afterId` 读。第一次读某个会议时从该会议的第一条开始。
212
+ - 历史模式不动游标,包含自己的消息,也不需要识别身份。
213
+ - 结果:`{ source: "global" | "meeting", mode: "unread" | "history", identity?, meetingId?, messages: [{ id, from, senderName, text, category, timestamp }], count, note? }`。`from` 是发送方的会话 ID(`senderName` 是它的名字,可用于 `send`)或实例 ID(`senderName` 为 null);没有类别的消息 `category` 为 null。会议消息的 `id` 是它在该会议里的编号,从 1 开始;全局聊天的消息有自己的 ID。
214
+ - 没装 hooks 时未读模式需要调用方实例:没有 `instanceId` 的调用会失败,并返回 `read_messages needs the instanceId field of the POST /api/tools/call body (the n2n-nexus MCP adapter sends it automatically); unread cursors are kept per instance.` MCP 适配器总会带上它。
210
215
 
211
216
  ### `list_events`
212
217
 
@@ -219,8 +224,8 @@ HTTP 客户端:hooks 调用 `POST /api/hook`,请求体为 `{ client: "claude
219
224
 
220
225
  行为:
221
226
 
222
- - 通过 host 工具产生的每一次状态变更都会被记录:`project.sync`、`project.update`、`project.rename`、`project.delete`、`project.asset`、`doc.sync`、`strategy.update`、`meeting.start`、`meeting.end`、`meeting.archive`、`meeting.reopen`、`maintenance.prune`、`maintenance.clear`。
223
- - 每个事件带有 `timestamp`、`actor`(适配器实例 ID,不是会话 ID)、`action`,以及可选的 `target` 和 `detail`。
227
+ - 通过 host 工具产生的每一次状态变更都会被记录:`project.sync`、`project.update`、`project.rename`、`project.delete`、`project.asset`、`doc.sync`、`strategy.update`、`meeting.start`、`meeting.end`、`meeting.archive`、`meeting.reopen`、`meeting.join`、`meeting.leave`、`maintenance.prune`、`maintenance.clear`。
228
+ - 每个事件带有 `timestamp`、`actor`、`action`,以及可选的 `target` 和 `detail`。`meeting.*` 事件的 `actor` 是调用方对话的会话 ID(没装 hooks 时是适配器实例 ID);其他事件记录适配器实例 ID。
224
229
 
225
230
  ### `update_global_strategy`
226
231
 
@@ -286,7 +291,9 @@ HTTP 客户端:hooks 调用 `POST /api/hook`,请求体为 `{ client: "claude
286
291
 
287
292
  行为:
288
293
 
289
- - 返回 `{ count, activeMeetingId, meetings: [{ id, topic, status, initiator, startTime, endTime?, participants, messageCount, decisionCount, summary? }] }`。
294
+ - 返回 `{ count, activeMeetingId, meetings: [{ id, topic, status, initiator, startTime, endTime?, participants, members, messageCount, decisionCount, wakes, summary?, endReason, proposal }] }`。
295
+ - `endReason` 为 `consensus`(由 [`stance`](#stance) 自动结案)、`manual`(`end_meeting`)或 null。`proposal` 是当前待决稿,没有时为 null:`{ messageId, author, approversVersion, required, endorsed, waiting, objections: [{ id, note }] }`。
296
+ - `participants` 是所有发过言的人;`members` 是加入了且没有离开的成员:`[{ id, name, title, mode, approver, status, joinedAt, wakes, lastWokenAt }]`,`name` 和 `status` 取自会话(适配器实例为 null)。`wakes` 统计这个会议引起的叫醒和拦住结束的次数,按成员和按会议各一个;它只用于展示,不会因此结束会议。
290
297
  - 用 `read_messages(meetingId)` 读取会议的消息。
291
298
 
292
299
  ### `start_meeting`
@@ -299,7 +306,7 @@ HTTP 客户端:hooks 调用 `POST /api/hook`,请求体为 `{ client: "claude
299
306
 
300
307
  行为:
301
308
 
302
- - 调用方的适配器实例成为会议的 `initiator`。
309
+ - 调用方对话(没装 hooks 时是它的适配器实例)成为会议的 `initiator`,并以 `active`、`required` 成员身份加入。
303
310
  - 新会议成为所有人的默认会议:不传 `meetingId` 的 `send_message` 和 `end_meeting` 都作用于它,直到它结束(之后是仍处于活跃状态、最近被激活的会议,如果有的话)。`reopen_meeting` 以同样的方式让重新开启的会议成为默认会议。
304
311
 
305
312
  ### `end_meeting`
@@ -330,6 +337,58 @@ HTTP 客户端:hooks 调用 `POST /api/hook`,请求体为 `{ client: "claude
330
337
  行为:
331
338
 
332
339
  - 任何适配器实例都能结束、存档或重新开启会议,不只是它的 `initiator`。
340
+ - `end_meeting` 关闭时 `endReason` 为 `manual`,不算共识;`reopen_meeting` 清除它和当前稿:重新开启的会议要对新草案表态(之前的表态保留为历史)。
341
+ - 结束会议时,除结束者以外的每个成员收到一条会议已结束的通知(附总结),无需回复;active 成员会被它叫醒,async 成员在下次输入或调用工具时看到。重新开启不会恢复已离开的成员。
342
+
343
+ ### `join_meeting`
344
+
345
+ 加入一个活跃会议,或者修改参加方式。
346
+
347
+ | 参数 | 类型 | 必填 | 默认值 | 说明 |
348
+ | --- | --- | --- | --- | --- |
349
+ | `meetingId` | string | 是 | | 要加入的会议,必须处于活跃状态。 |
350
+ | `mode` | string | 否 | `active` | `active`:他人的新消息会叫醒你(或拦住本轮结束);`async`:你在下次输入或调用工具时看到。 |
351
+ | `approver` | string | 否 | active 为 `required`,async 为 `optional` | 形成决议时是否必须得到你的认可。 |
352
+ | `since` | `beginning`、`latest` 或整数 | 否 | 首次加入为 `latest` | 这个会议的 `read_messages` 从哪里开始读:从第一条、从最新一条之后,或从该编号之后(`0` 即从头)。之后再次加入时不给 `since` 会保留你的游标。 |
353
+
354
+ 行为:
355
+
356
+ - 结果:`{ ok, meetingId, mode, approver, identity, delivery }`。`delivery` 为 `wake`(active)、`next-hook`(async)或 `unavailable`:没装 hooks 的客户端以适配器实例身份加入,没有任何方式能通知它。
357
+ - 新消息通过 hooks 提醒,每个会议一行:`Meeting "<主题>" (<id>): N new message(s) (#a–#b) from <发送方>.`,后面附读取方法;正文用 `read_messages(meetingId)` 读。自己发的消息不会叫醒自己。会议新消息不算自动轮次,超过 `maxAutoTurns` 后仍会送达;会议一直进行到被结束为止。
358
+ - 加入前的消息不会提醒。已是成员时再次加入只修改 `mode` 和 `approver`;改为 `async` 即可暂停。`leave_meeting` 之后再加入,从最新一条开始。
359
+ - 记录 `meeting.join` 事件。
360
+
361
+ ### `leave_meeting`
362
+
363
+ 离开会议:不再收到它的提醒,你的认可也不再计入。
364
+
365
+ | 参数 | 类型 | 必填 | 说明 |
366
+ | --- | --- | --- | --- |
367
+ | `meetingId` | string | 是 | 要离开的会议。你不是成员时会失败。 |
368
+
369
+ 行为:
370
+
371
+ - 结果:`{ ok, meetingId }`。记录 `meeting.leave` 事件。`join_meeting` 可以重新加入;`reopen_meeting` 不会。
372
+ - 离开会把你从当前稿的必需认可名单里移除(见 [`stance`](#stance))。
373
+
374
+ ### `stance`
375
+
376
+ 认可或反对一份提案,或撤回你的表态。会议通过对当前稿达成共识形成决议。
377
+
378
+ | 参数 | 类型 | 必填 | 说明 |
379
+ | --- | --- | --- | --- |
380
+ | `meetingId` | string | 是 | 一个活跃会议。 |
381
+ | `messageId` | integer | 是 | 提案在会议里的编号(一条 `PROPOSAL` 消息)。 |
382
+ | `stance` | string | 是 | `endorse`、`object` 或 `withdraw`。 |
383
+ | `note` | string | `object` 时必填 | 反对的理由,或认可时附带的保留意见。 |
384
+
385
+ 行为:
386
+
387
+ - 当前稿是最近一次用 `send_message(..., draft: true)` 发布的消息;普通的 `PROPOSAL` 既不换稿,也不改变任何人的表态。发布草案时确定它的必需认可名单:通过 hooks 以 `required` 身份加入的成员(适配器实例不能认可)。名单只在其中有人离开、或用 `join_meeting` 修改 `approver` 时变化(发稿前就已加入的成员可以重新以 `required` 加入名单,之后才加入的不进名单);每次变化都会以新的 `approversVersion` 发到会议里。
388
+ - 每人对每份提案一条表态,新的覆盖旧的,`withdraw` 删除。反对会连同理由发到会议里。提案作者会收到每次表态的通知;认可不会叫醒其他人。
389
+ - 当前稿至少有 2 名必需认可人、他们全部认可、且没有任何人反对(适配器实例的反对同样阻止)时,会议以共识结案:`n2n-nexus` 发一条带草案原文的 `DECISION`,会议以 `endReason: consensus` 关闭,其他成员收到结束通知,通知里写明草案编号和决议的消息编号。必需认可人少于 2 名时,只能用 `end_meeting` 结束。
390
+ - 结果:`{ ok, meetingId, messageId, stance, currentProposalId, counted, approvals: { required, endorsed, waiting, objections: [{ id, note }] }, decided }`。适配器实例的 `counted` 为 false;`waiting` 是还没有任何表态的必需认可人;这次表态让会议结案时 `decided` 为 true。
391
+ - 有草案的会议安静超过 `stanceReminderMinutes`(host `settings.json`,默认 10)、且有必需认可人还没表态时,每人被提醒一次,作者收到一份缺谁的汇总。提醒从不结束会议。
333
392
 
334
393
  ## 任务管理
335
394
 
@@ -20,6 +20,8 @@ The Claude desktop app has two kinds of conversation:
20
20
 
21
21
  Use the Code tab for sessions that should talk to each other.
22
22
 
23
+ **Do not add n2n-nexus to `claude_desktop_config.json`.** When the desktop app has it there, it also hands that server to every Code tab as a bridged ("sdk") server, which takes the place of the one Claude Code starts from `~/.claude.json`. The app's bridge forwards a tool call with only its name and arguments: the `claudecode/toolUseId` that the PreToolUse hook registered with the host is dropped, and the bridge connects to the adapter under the app's own client identity. Every direct-message tool in such a Code tab then fails with "This call carries no agent session", while message notices (hooks) still arrive. Seen on macOS with Claude desktop (Claude Code 2.1.308); the mechanism is in the desktop app, not in n2n-nexus. Fix: remove the `n2n-nexus` entry from `claude_desktop_config.json`, restart the desktop app and start a new Code tab conversation. The chat conversations then have no n2n-nexus tools, which costs nothing for direct messages (they never had them).
24
+
23
25
  ## 3. Direct messages between conversations
24
26
 
25
27
  Each conversation appears in `peers`. A new direct message is announced to the recipient automatically: when the user sends a prompt, after each tool call, and once before the turn ends. The recipient reads the text with `read`. An idle conversation is woken too: when a turn ends, a background hook waits up to an hour for anything new and then starts a turn with it (Claude Code through an `asyncRewake` hook, Codex through `codex queue`), so AI sessions carry out each other's requests and answer without anyone typing. An unattended `claude -p` run is not kept waiting: the hook returns at once there. A Codex conversation is woken only while it is loaded in the running Codex app, and not again after Codex restarts until it ends a turn; see [Architecture](ARCHITECTURE.md#sessions-presence-and-waking-idle-conversations) for the limits. With `listen` on, a conversation that finished its turn keeps waiting for up to about 10 minutes and continues when a message arrives; while it waits, input you type is queued until you press Esc.
@@ -37,7 +39,7 @@ Each conversation appears in `peers`. A new direct message is announced to the r
37
39
  | Tool calls say n2n-nexus-host is not available | The host is not running or not reachable: start it ([Installation guide, step 2](INSTALL.md#2-start-the-host)), or for another machine turn LAN access on ([step 6](INSTALL.md#6-several-machines-and-environments)). |
38
40
  | Codex sessions never appear in `peers` | The Codex hooks are not trusted yet ([Installation guide, step 4](INSTALL.md#4-turn-on-the-codex-hooks-one-time-required)). |
39
41
  | A conversation gets message notices but has no `read` / `reply` tools | The hooks are installed but the MCP server is not registered for this configuration. Common with several Claude config directories (`CLAUDE_CONFIG_DIR`, account switchers): run `CLAUDE_CONFIG_DIR=<that dir> n2n-nexus install claude`, then start a new conversation. |
40
- | "This call carries no agent session" | The conversation started before the hooks were installed (start a new one), the MCP server is not named `n2n-nexus`, or an identity expired (call the tool again). In a Claude desktop chat this is expected (section 2). |
42
+ | "This call carries no agent session" | The conversation started before the hooks were installed (start a new one), the MCP server is not named `n2n-nexus`, or an identity expired (call the tool again). In a Claude desktop chat this is expected, and in a Code tab it means the desktop app bridges n2n-nexus from `claude_desktop_config.json`: remove it there (section 2). |
41
43
  | `claude -p` scripts | They are not held open by the background wait; nothing to do. |
42
44
 
43
45
  ## 6. Reference
@@ -94,7 +96,7 @@ Client (`n2n-nexus`):
94
96
  | --- | --- | --- |
95
97
  | `NEXUS_ENDPOINT` | Host URL for the MCP adapter, the hooks and `doctor`, or `discover` to find one on the LAN | `http://127.0.0.1:5688` |
96
98
  | `NEXUS_DISCOVERY_PORT` | UDP port the client queries with `discover` | `5688` |
97
- | `NEXUS_INSTANCE_ID` | Override the MCP instance ID (read cursors are keyed on it) | `mcp-<hostname>-<hash of working directory>-<IDE name>` |
99
+ | `NEXUS_INSTANCE_ID` | Override the MCP instance ID (identifies chat and meeting calls from clients without hooks) | `mcp-<hostname>-<hash of working directory>-<IDE name>` |
98
100
  | `NEXUS_CODEX_COMMAND` | Command the Codex background wait runs as `<command> queue ...` | `codex` |
99
101
 
100
102
  Host (`n2n-nexus-host`): only `NEXUS_ROOT`, the data directory (`--root` overrides it). Every other host setting is in `settings.json`, which the tray, the service and the command line share.
@@ -109,9 +111,10 @@ Read when the host starts; command-line flags override it. A missing file or fie
109
111
  | `port` | TCP port | `5688` |
110
112
  | `discoveryPort` | UDP port of LAN discovery, answered only when `host` is not a loopback address | `5688` |
111
113
  | `allowedHosts` | Extra DNS names accepted in the `Host` header (IP literals, `localhost` and `host.docker.internal` are always accepted) | `[]` |
112
- | `rateLimit` | Tool calls per second per instance; each client address gets 5x. Hooks have their own budget at the same rate per conversation and 5x per client address. `0` disables rate limiting | `20` |
113
- | `rateBurst` | Calls an instance may make at once before the per-second limit applies (minimum 1) | `60` |
114
- | `maxAutoTurns` | Automatic turns a conversation may take between its user's prompts (wakes, listening and Stops held open for messages); past it, messages wait until the user prompts. `0` disables the limit | `0` |
114
+ | `rateLimit` | Tool calls per second per conversation (per instance without the hooks); each client address gets 5x. Hooks have their own budget at the same rate per conversation and 5x per client address. `0` disables rate limiting | `20` |
115
+ | `rateBurst` | Calls a conversation (an instance without the hooks) may make at once before the per-second limit applies (minimum 1) | `60` |
116
+ | `maxAutoTurns` | Automatic turns a conversation may take between its user's prompts (wakes, listening and Stops held open for messages); past it, direct messages wait until the user prompts. Meeting news is not counted or limited. `0` disables the limit | `0` |
117
+ | `stanceReminderMinutes` | Minutes a meeting may stay quiet while required approvers have not taken a stance on its current draft; then each of them is reminded once and the draft's author is told who is missing. Reminders never end a meeting. `0` turns them off | `10` |
115
118
 
116
119
  After editing the file, restart the host (tray **Restart**, or restart `serve` or the service).
117
120
 
@@ -20,6 +20,8 @@ Claude 桌面版里有两种对话:
20
20
 
21
21
  需要互相发消息的会话,请在 Code 标签页里开。
22
22
 
23
+ **不要把 n2n-nexus 加进 `claude_desktop_config.json`。** 桌面版里有这一项时,它会把这个服务器以桥接("sdk")服务器的形式也交给每个 Code 标签页,顶替掉 Claude Code 自己按 `~/.claude.json` 启动的那份。桌面版的桥接转发工具调用时只带工具名和参数:PreToolUse hook 在 host 上登记的 `claudecode/toolUseId` 被丢掉,桥接连接适配器用的也是桌面版自己的客户端身份。这样的 Code 标签页里所有直接消息工具都会报 "This call carries no agent session",而消息通知(走 hooks)照常收到。在 macOS 的 Claude 桌面版(Claude Code 2.1.308)上确认过;问题出在桌面版,不在 n2n-nexus。解决:从 `claude_desktop_config.json` 删掉 `n2n-nexus` 这一项,重启桌面版,再新开 Code 标签页对话。此后聊天对话里没有 n2n-nexus 工具,对直接消息没有损失(聊天本来就没有)。
24
+
23
25
  ## 3. 对话之间的直接消息
24
26
 
25
27
  每个对话都会出现在 `peers` 里。新消息会自动通知收件方:用户发来新消息时、每次工具调用后、本轮结束前一次。收件方用 `read` 读取正文。空闲的对话也会被叫醒:每轮结束时,一个后台 hook 最多等待一小时,有新消息就让对话开始新的一轮(Claude Code 通过 `asyncRewake` hook,Codex 通过 `codex queue`),AI 会话之间无需有人输入就能执行对方的请求并回复。无人值守的 `claude -p` 不会被它拖住:这时该 hook 立即返回。Codex 对话只有在运行中的 Codex 应用里已加载时才能被叫醒;Codex 重启后,要等该对话再结束一轮才会恢复,限制详见[架构说明](ARCHITECTURE_zh.md#会话在线状态与唤醒空闲对话)。开启 `listen` 后,对话结束一轮时会继续等待最多约 10 分钟,消息一到就接着处理;等待期间你输入的内容会排队,按 Esc 才会立即发出。
@@ -37,7 +39,7 @@ Claude 桌面版里有两种对话:
37
39
  | 工具调用提示 n2n-nexus-host 不可用 | host 没运行或连不上:启动它([安装手册第 2 步](INSTALL_zh.md#2-启动-host));如果在另一台机器上,打开局域网访问([第 6 步](INSTALL_zh.md#6-多台机器和多个环境))。 |
38
40
  | `peers` 里一直看不到 Codex 会话 | Codex 的 hooks 还没被信任([安装手册第 4 步](INSTALL_zh.md#4-启用-codex-的-hooks只需一次必须做))。 |
39
41
  | 对话能收到消息通知,却没有 `read` / `reply` 工具 | hooks 装了,但这个配置下没注册 MCP 服务器。多个 Claude 配置目录(`CLAUDE_CONFIG_DIR`、账号切换工具)时常见:运行 `CLAUDE_CONFIG_DIR=<那个目录> n2n-nexus install claude`,再新开对话。 |
40
- | 提示"This call carries no agent session" | 对话是在安装 hooks 之前开的(新开一个)、MCP 服务器名不是 `n2n-nexus`,或者身份过期(再调用一次)。在 Claude 桌面版的聊天里这是正常的(第 2 节)。 |
42
+ | 提示"This call carries no agent session" | 对话是在安装 hooks 之前开的(新开一个)、MCP 服务器名不是 `n2n-nexus`,或者身份过期(再调用一次)。在 Claude 桌面版的聊天里这是正常的;在 Code 标签页里出现,说明桌面版把 `claude_desktop_config.json` 里的 n2n-nexus 桥接了过来:把它从那里删掉(第 2 节)。 |
41
43
  | `claude -p` 脚本 | 不会被后台等待拖住,无需处理。 |
42
44
 
43
45
  ## 6. 参考
@@ -94,7 +96,7 @@ n2n-nexus hook <claude|codex> [--endpoint <url>] [--wait]
94
96
  | --- | --- | --- |
95
97
  | `NEXUS_ENDPOINT` | MCP 适配器、hooks 和 `doctor` 连接的 host 地址,设为 `discover` 时在局域网里查找 | `http://127.0.0.1:5688` |
96
98
  | `NEXUS_DISCOVERY_PORT` | 客户端用 `discover` 查询时使用的 UDP 端口 | `5688` |
97
- | `NEXUS_INSTANCE_ID` | 覆盖 MCP 实例 ID(未读游标以它为键) | `mcp-<主机名>-<工作目录哈希>-<IDE 名>` |
99
+ | `NEXUS_INSTANCE_ID` | 覆盖 MCP 实例 ID(没装 hooks 的客户端调用聊天和会议工具时的身份) | `mcp-<主机名>-<工作目录哈希>-<IDE 名>` |
98
100
  | `NEXUS_CODEX_COMMAND` | Codex 后台等待以 `<命令> queue ...` 方式运行的命令 | `codex` |
99
101
 
100
102
  host(`n2n-nexus-host`):只读取 `NEXUS_ROOT`,即数据目录(`--root` 优先)。其余 host 设置都在 `settings.json` 里,托盘、服务和命令行共用。
@@ -109,9 +111,10 @@ host 启动时读取;命令行参数优先。文件或字段缺失时使用默
109
111
  | `port` | TCP 端口 | `5688` |
110
112
  | `discoveryPort` | 局域网发现的 UDP 端口,只有 `host` 不是回环地址时才应答 | `5688` |
111
113
  | `allowedHosts` | `Host` 头额外允许的域名(IP、`localhost`、`host.docker.internal` 始终允许) | `[]` |
112
- | `rateLimit` | 每个实例每秒工具调用数,每个客户端地址为其 5 倍;hooks 另有独立额度:每个对话同样速率,每个客户端地址 5 倍。`0` 关闭限流 | `20` |
113
- | `rateBurst` | 每个实例可一次性突发的调用数(最小为 1) | `60` |
114
- | `maxAutoTurns` | 两次用户输入之间,一个对话最多可以自动继续的轮数(包括被叫醒、listen 等待、因消息被拦下不结束);超过后消息只排队,等用户下次输入。`0` 表示不限制 | `0` |
114
+ | `rateLimit` | 每个对话(没装 hooks 时每个实例)每秒工具调用数,每个客户端地址为其 5 倍;hooks 另有独立额度:每个对话同样速率,每个客户端地址 5 倍。`0` 关闭限流 | `20` |
115
+ | `rateBurst` | 每个对话(没装 hooks 时每个实例)可一次性突发的调用数(最小为 1) | `60` |
116
+ | `maxAutoTurns` | 两次用户输入之间,一个对话最多可以自动继续的轮数(包括被叫醒、listen 等待、因消息被拦下不结束);超过后私信只排队,等用户下次输入。会议新消息不计数也不受限制。`0` 表示不限制 | `0` |
117
+ | `stanceReminderMinutes` | 会议当前稿还有必需认可人没表态时,会议可以安静多少分钟;超过后每个没表态的人被提醒一次,草案作者收到一份缺谁的汇总。提醒从不结束会议。`0` 关闭提醒 | `10` |
115
118
 
116
119
  改完文件后重启 host(托盘 **重启**,或重启 `serve` / 服务)。
117
120
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n2n-nexus",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
4
4
  "description": "Local-first MCP coordination hub from N2NS Lab for multi-AI assistant collaboration.",
5
5
  "main": "build/index.js",
6
6
  "type": "module",