n2n-nexus 0.9.2 → 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.
@@ -1,18 +1,20 @@
1
1
  # n2n-nexus tools reference
2
2
 
3
+ [中文版](TOOLS_REFERENCE_zh.md)
4
+
3
5
  This document describes the MCP tools exposed by the n2n-nexus adapter. Tool calls are forwarded to the host (`n2n-nexus-host`) running at `NEXUS_ENDPOINT`.
4
6
 
5
7
  Parameter tables below are derived from `contract/tools.json`, the JSON Schema the host serves through `GET /api/tools` and validates every call against before a handler runs. Invalid arguments are rejected with a descriptive error (`Invalid arguments for <tool>: ...`); keys the schema does not declare are dropped before validation.
6
8
 
7
9
  ## Conventions
8
10
 
9
- - 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.
10
- - 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.
11
13
  - Every tool has a display `title` (for example "Delete Project") for client UIs.
12
- - 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.
13
- - 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 and meeting initiator permissions 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.
14
- - 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`.
15
- - Project IDs should follow `[prefix]_[name]`, for example `web_example.com`, `api_user-auth`, or `mcp_nexus`.
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
+ - Project IDs follow `[prefix]_[name]`; the prefixes are listed in the [Assistant guide](ASSISTANT_GUIDE.md#2-project-ids).
16
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.
17
19
  - Project tools take `projectId` explicitly.
18
20
  - `host_delete_project` is destructive and irreversible.
@@ -148,25 +150,25 @@ These six tools are direct messages between conversations. Global chat and meeti
148
150
  | Tool | Parameters | Result and behavior |
149
151
  | --- | --- | --- |
150
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. |
151
- | `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). |
152
- | `read` | `count?: integer = 10` (1–50) | `{ messages: [{ messageId, from: { id, name }, replyTo, text, createdAt }], remaining }`; unread messages oldest first, marked read. |
153
- | `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. |
154
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. |
155
- | `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 `""`. |
156
158
 
157
- `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.
158
160
 
159
- 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)):
160
162
 
161
163
  | Client event | What the recipient sees |
162
164
  | --- | --- |
163
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`. |
164
- | Prompt submitted | Count of unread messages, up to five listed, and pending notices. |
165
- | After each tool call | Only messages and notices not announced yet. |
166
- | Turn about to end | If anything is unread, the turn continues once with the announcement. |
167
- | Listening | The wait ends as soon as something new arrives, and the conversation continues with it. |
168
- | 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. |
169
- | 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`. After `maxAutoTurns` (host `settings.json`, default 8) 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. |
170
172
 
171
173
  Unread messages expire after 24 hours; the sender then gets a notice. Notices also report `notifyWhenIdle` results.
172
174
 
@@ -181,6 +183,7 @@ Post to a meeting or to global chat. For a message to one conversation, use `sen
181
183
  | `message` | string | yes | Message body. |
182
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. |
183
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). |
184
187
 
185
188
  Behavior:
186
189
 
@@ -188,23 +191,27 @@ Behavior:
188
191
  - Otherwise, if a meeting is active, the message goes to it.
189
192
  - Otherwise it is written to global chat.
190
193
  - A `DECISION` message in a meeting is also recorded in that meeting's decisions (`decisionCount` in `list_meetings`).
191
- - The sender stored with the message is the calling adapter instance ID.
192
- - 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`).
193
196
 
194
197
  ### `read_messages`
195
198
 
196
- 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`.
197
200
 
198
201
  | Parameter | Type | Required | Default | Notes |
199
202
  | --- | --- | --- | --- | --- |
200
203
  | `count` | integer | no | `10` | Number of messages to read (1–500). |
201
- | `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`. |
202
207
 
203
208
  Behavior:
204
209
 
205
- - 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.
206
- - Each message's `from` is the sending adapter instance ID, not a session ID: you cannot `send` to it.
207
- - 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.
208
215
 
209
216
  ### `list_events`
210
217
 
@@ -217,8 +224,8 @@ Read the structured activity log.
217
224
 
218
225
  Behavior:
219
226
 
220
- - 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`.
221
- - 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.
222
229
 
223
230
  ### `update_global_strategy`
224
231
 
@@ -284,7 +291,9 @@ List meetings, newest first.
284
291
 
285
292
  Behavior:
286
293
 
287
- - 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.
288
297
  - Read a meeting's messages with `read_messages(meetingId)`.
289
298
 
290
299
  ### `start_meeting`
@@ -297,7 +306,7 @@ Open a new meeting session.
297
306
 
298
307
  Behavior:
299
308
 
300
- - 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.
301
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.
302
311
 
303
312
  ### `end_meeting`
@@ -327,7 +336,59 @@ Reactivate a closed or archived meeting.
327
336
 
328
337
  Behavior:
329
338
 
330
- - Only the adapter instance that started the meeting (its `initiator`) can end, archive or reopen it; calls from any other instance fail with `Only the MCP adapter instance that started this meeting (<initiator>) can <action> it; this call comes from instance <caller>.`
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.
331
392
 
332
393
  ## Task management
333
394