n2n-nexus 0.12.4 → 0.12.5

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@
4
4
 
5
5
  All notable changes to this project will be documented in this file.
6
6
 
7
+ ## [0.12.5] - 2026-11-04
8
+
9
+ Upgrade the host; the client needs no update for these tools (contract version 4). The database is upgraded in place: keep `nexus.db`. An older host refuses the upgraded database.
10
+
11
+ ### Added
12
+
13
+ - `broadcast` sends one notice to every conversation that is online at that moment, the sender excepted; `project` limits it to the conversations working in that directory or below it. It is fire and forget: the host picks the recipients and delivers it, the sender gets only a `broadcastId` and learns nothing about who got or read it, and offline conversations or ones that start later never get it. The host stores it once with one delivery per recipient, and every recipient sees it under the same id, in a `--- Broadcast <id> from <sender> ... ---` header. A recipient that is listening for messages is woken with it at once; a working one is not interrupted (its turn is not held) and sees it after its next tool call, and an idle one at its next prompt or tool call. It needs no reply unless the sender sets `expectsReply`, and a `reply` on that id goes to the sender alone, with the `broadcastId` in `replyTo`. An unread broadcast expires after 24 hours like a direct message, without telling anyone. `read` returns `kind` (`direct` or `broadcast`), `scope` and `replyToKind` for each message.
14
+ - `send_message` takes `global: true` to post to the global chat while a meeting is active; before, a post without `meetingId` always went to the active meeting.
15
+
16
+ ### Changed
17
+
18
+ - `host_maintenance` cleanup counts broadcast deliveries too: `messagesExpired` includes the deliveries it marked expired, `messages` the ones it deleted.
19
+
7
20
  ## [0.12.4] - 2026-10-30
8
21
 
9
22
  ### Fixed
package/README.md CHANGED
@@ -108,7 +108,7 @@ The host exposes 36 tools, grouped by purpose:
108
108
  | **Projects & assets** | `search_projects`, `get_project`, `update_project`, `rename_project`, `sync_project_assets`, `upload_project_asset` | Find, read, update, and rename projects; sync manifests and internal docs; upload binary assets. |
109
109
  | **Global docs** | `list_global_docs`, `get_global_doc`, `sync_global_doc`, `get_global_strategy`, `update_global_strategy` | List, read, create and update shared global documents and the master strategy document. |
110
110
  | **Messaging** | `send_message`, `read_messages`, `list_events` | Post meeting or global messages; read them and the activity log. |
111
- | **Direct messages** | `peers`, `send`, `read`, `reply`, `listen`, `set_profile` | Find other Claude Code / Codex conversations, message them, read and answer what arrives, wait for messages, and name yourself. |
111
+ | **Direct messages** | `peers`, `send`, `read`, `reply`, `broadcast`, `listen`, `set_profile` | Find other Claude Code / Codex conversations, message them, read and answer what arrives, notify every online conversation, wait for messages, and name yourself. |
112
112
  | **Meetings** | `list_meetings`, `start_meeting`, `join_meeting`, `leave_meeting`, `stance`, `end_meeting`, `archive_meeting`, `reopen_meeting` | List and open meetings; join (woken by new messages) or leave them; endorse or object to the current draft, which closes the meeting by consensus or, with the user's seat, is decided by the user; close, archive, and reopen them. |
113
113
  | **Tasks** | `create_task`, `get_task`, `list_tasks`, `update_task`, `cancel_task` | Create, update and track tasks that assistants manage themselves. |
114
114
  | **Maintenance** | `host_maintenance`, `host_delete_project` | Prune or clear the global chat stream; run the retention cleanup now; export a meeting as markdown; delete a project. |
@@ -73,6 +73,10 @@
73
73
  "messageHeader": "--- Message %s from %s (%s) at %s ---",
74
74
  "messageEnd": "--- End of message %s ---",
75
75
  "messageReplyTo": "This answers your message %s.",
76
+ "messageReplyToBroadcast": "This answers your broadcast %s.",
77
+ "broadcastHeader": "--- Broadcast %s from %s (%s) at %s, sent to %s ---",
78
+ "broadcastToAll": "every online session",
79
+ "broadcastToProject": "the online sessions working in %s",
76
80
  "messageCut": "[Cut at %d of %d characters; read returns the whole message.]",
77
81
  "replyInstruction": "Answer it with reply (messageId %s; expectsReply false for a result, true for a question).",
78
82
  "readRest": "read returns the messages that are cut or not shown here.",
@@ -1213,7 +1213,7 @@
1213
1213
  {
1214
1214
  "name": "read",
1215
1215
  "title": "Read Direct Messages",
1216
- "description": "Read your unread direct messages from other AI sessions of your own user, oldest first, and mark them read; meetings and the global chat are read with read_messages instead. A notification shows each message whole; one that needs no reply is marked read there when the notification comes with a prompt, and every message shown whole is when it wakes you or holds your Stop. read returns the ones cut or not shown, those shown after a tool call, and those shown with a prompt that you have not answered yet. When a message asks for an answer (expectsReply true), answer once with reply.",
1216
+ "description": "Read your unread direct messages and broadcasts from other AI sessions of your own user, oldest first, and mark them read; meetings and the global chat are read with read_messages instead. A notification shows each message whole; one that needs no reply is marked read there when the notification comes with a prompt, and every message shown whole is when it wakes you or holds your Stop. read returns the ones cut or not shown, those shown after a tool call, and those shown with a prompt that you have not answered yet. When a message asks for an answer (expectsReply true), answer once with reply.",
1217
1217
  "annotations": {
1218
1218
  "readOnlyHint": false,
1219
1219
  "destructiveHint": false,
@@ -1242,6 +1242,14 @@
1242
1242
  "messageId": {
1243
1243
  "type": "string"
1244
1244
  },
1245
+ "kind": {
1246
+ "type": "string",
1247
+ "enum": [
1248
+ "direct",
1249
+ "broadcast"
1250
+ ],
1251
+ "description": "broadcast: a broadcast you received; messageId is then the broadcast's id"
1252
+ },
1245
1253
  "from": {
1246
1254
  "type": "object",
1247
1255
  "properties": {
@@ -1258,12 +1266,31 @@
1258
1266
  ],
1259
1267
  "additionalProperties": false
1260
1268
  },
1269
+ "scope": {
1270
+ "type": [
1271
+ "string",
1272
+ "null"
1273
+ ],
1274
+ "description": "The working directory a broadcast was limited to; null for every online session and for a direct message"
1275
+ },
1261
1276
  "replyTo": {
1262
1277
  "type": [
1263
1278
  "string",
1264
1279
  "null"
1265
1280
  ]
1266
1281
  },
1282
+ "replyToKind": {
1283
+ "type": [
1284
+ "string",
1285
+ "null"
1286
+ ],
1287
+ "enum": [
1288
+ "direct",
1289
+ "broadcast",
1290
+ null
1291
+ ],
1292
+ "description": "What replyTo names: a direct message, or a broadcast (its broadcastId); null when replyTo is"
1293
+ },
1267
1294
  "text": {
1268
1295
  "type": "string"
1269
1296
  },
@@ -1276,11 +1303,14 @@
1276
1303
  },
1277
1304
  "required": [
1278
1305
  "messageId",
1306
+ "kind",
1279
1307
  "from",
1280
1308
  "replyTo",
1281
1309
  "text",
1282
1310
  "createdAt",
1283
- "expectsReply"
1311
+ "expectsReply",
1312
+ "scope",
1313
+ "replyToKind"
1284
1314
  ],
1285
1315
  "additionalProperties": false
1286
1316
  }
@@ -1303,7 +1333,7 @@
1303
1333
  {
1304
1334
  "name": "reply",
1305
1335
  "title": "Reply to Message",
1306
- "description": "Reply to a direct message you received (for a meeting or the global chat use send_message instead); it goes to the original sender, and the message you answer counts as read. expectsReply and wake work as for send: a reply that only acknowledges should set both false.",
1336
+ "description": "Reply to a direct message or a broadcast you received (for a meeting or the global chat use send_message instead); it goes to the original sender as a direct message, and the message you answer counts as read. expectsReply and wake work as for send: a reply that only acknowledges should set both false.",
1307
1337
  "annotations": {
1308
1338
  "readOnlyHint": false,
1309
1339
  "destructiveHint": false,
@@ -1316,7 +1346,7 @@
1316
1346
  "messageId": {
1317
1347
  "type": "string",
1318
1348
  "minLength": 1,
1319
- "description": "A message you received, from a notification or read"
1349
+ "description": "A message or broadcast you received (its messageId), from a notification or read"
1320
1350
  },
1321
1351
  "text": {
1322
1352
  "type": "string",
@@ -1381,6 +1411,56 @@
1381
1411
  },
1382
1412
  "publishOutputSchema": false
1383
1413
  },
1414
+ {
1415
+ "name": "broadcast",
1416
+ "title": "Broadcast Message",
1417
+ "description": "Send one notice to every AI session of your own user that is online now (not offline), yourself excepted; project limits it to the sessions working in that directory or below it. It is fire and forget, like a network broadcast: the host picks the recipients when you call it and delivers the message, and you learn nothing about who got or read it; sessions that are offline, or that start later, never get it. Every recipient sees it under the same id, the broadcastId, as a message of kind broadcast from you. A recipient that is listening (waiting for messages) is woken with it at once; a working or idle one is not interrupted and sees it at its next tool call or prompt. By default it needs no reply; with expectsReply true each recipient is asked to answer you with reply on that id, which reaches you alone with the broadcastId in replyTo. Calling it again sends it again. Needs the n2n-nexus hooks.",
1418
+ "annotations": {
1419
+ "readOnlyHint": false,
1420
+ "destructiveHint": false,
1421
+ "idempotentHint": false,
1422
+ "openWorldHint": false
1423
+ },
1424
+ "inputSchema": {
1425
+ "type": "object",
1426
+ "properties": {
1427
+ "text": {
1428
+ "type": "string",
1429
+ "minLength": 1,
1430
+ "maxLength": 32000,
1431
+ "description": "Plain text, shown whole in each recipient's notification (a long message is cut there, and read returns it)."
1432
+ },
1433
+ "project": {
1434
+ "type": "string",
1435
+ "minLength": 1,
1436
+ "maxLength": 1000,
1437
+ "description": "Only sessions whose working directory is this path or below it (whole path components, compared as written)"
1438
+ },
1439
+ "expectsReply": {
1440
+ "type": "boolean",
1441
+ "default": false,
1442
+ "description": "true: ask every recipient to answer you with reply."
1443
+ }
1444
+ },
1445
+ "required": [
1446
+ "text"
1447
+ ]
1448
+ },
1449
+ "outputSchema": {
1450
+ "type": "object",
1451
+ "properties": {
1452
+ "broadcastId": {
1453
+ "type": "string",
1454
+ "description": "Names this broadcast: its recipients see it as the broadcast's messageId, and their replies name it in replyTo"
1455
+ }
1456
+ },
1457
+ "required": [
1458
+ "broadcastId"
1459
+ ],
1460
+ "additionalProperties": false
1461
+ },
1462
+ "publishOutputSchema": false
1463
+ },
1384
1464
  {
1385
1465
  "name": "listen",
1386
1466
  "title": "Listen for Messages",
@@ -1549,7 +1629,7 @@
1549
1629
  {
1550
1630
  "name": "send_message",
1551
1631
  "title": "Post Message",
1552
- "description": "Post to a meeting or to the global chat; a direct message to one session is send (or reply). Post to a meeting (meetingId, else the active meeting) or, when no meeting is active, to the shared global chat that every session can read with read_messages. The sender is this conversation's session id (claude:<id> / codex:<id>) when the n2n-nexus hooks are installed, else the MCP adapter instance; identity says which. messageId is the message's number in its meeting (from 1), or its global chat id. draft: true publishes the message as the meeting's current draft (a PROPOSAL; currentProposalId is its number): its required approvers are the members that are required approvers and conversation sessions at that moment, and the members take a stance on it with stance. Only a draft replaces the current draft and starts its stances afresh; a PROPOSAL sent without draft changes neither. Before posting to a meeting, read what others said: when messages of others came after you last read the meeting (read_messages in unread mode, a notification that showed them whole, or an earlier refusal; history reads do not count), the message is not sent: ok is false, refused true and unread has those messages, which now count as read. Take them into account rather than resending the same text. The next send_message to that meeting goes through even if more messages arrived, with a note of the unread numbers appended to its text; a draft or a DECISION never does: it is posted only once everything is read. Global chat has no such check.",
1632
+ "description": "Post to a meeting or to the global chat; a direct message to one session is send (or reply). Post to a meeting (meetingId, else the active meeting) or, when no meeting is active or global is true, to the shared global chat that every session can read with read_messages; nobody is notified of a global chat message, broadcast notifies every online session. The sender is this conversation's session id (claude:<id> / codex:<id>) when the n2n-nexus hooks are installed, else the MCP adapter instance; identity says which. messageId is the message's number in its meeting (from 1), or its global chat id. draft: true publishes the message as the meeting's current draft (a PROPOSAL; currentProposalId is its number): its required approvers are the members that are required approvers and conversation sessions at that moment, and the members take a stance on it with stance. Only a draft replaces the current draft and starts its stances afresh; a PROPOSAL sent without draft changes neither. Before posting to a meeting, read what others said: when messages of others came after you last read the meeting (read_messages in unread mode, a notification that showed them whole, or an earlier refusal; history reads do not count), the message is not sent: ok is false, refused true and unread has those messages, which now count as read. Take them into account rather than resending the same text. The next send_message to that meeting goes through even if more messages arrived, with a note of the unread numbers appended to its text; a draft or a DECISION never does: it is posted only once everything is read. Global chat has no such check.",
1553
1633
  "annotations": {
1554
1634
  "readOnlyHint": false,
1555
1635
  "destructiveHint": false,
@@ -1576,13 +1656,17 @@
1576
1656
  "description": "Optional label. DECISION is also added to the meeting's decisions list. Omitted = no category."
1577
1657
  },
1578
1658
  "meetingId": {
1579
- "description": "Target meeting. Defaults to the active meeting, or global chat when none is active.",
1659
+ "description": "Target meeting. Defaults to the active meeting, or global chat when none is active (global true posts there anyway).",
1580
1660
  "type": "string",
1581
1661
  "minLength": 1
1582
1662
  },
1583
1663
  "draft": {
1584
1664
  "type": "boolean",
1585
1665
  "description": "Publish as the meeting's current draft for the members to endorse or object to (meetings only; category is PROPOSAL, any other category fails)."
1666
+ },
1667
+ "global": {
1668
+ "type": "boolean",
1669
+ "description": "true: post to the global chat even while a meeting is active (not with meetingId or draft)."
1586
1670
  }
1587
1671
  },
1588
1672
  "required": [
@@ -3647,7 +3731,7 @@
3647
3731
  {
3648
3732
  "name": "host_maintenance",
3649
3733
  "title": "Clean Up the Host",
3650
- "description": "Host housekeeping. 'prune' removes the oldest count global chat messages (count ≥ 1 required), 'clear' removes all of them. 'cleanup' runs the hourly cleanup now: tasks past their TTL fail; finished tasks, events, read or expired direct messages, delivered notices, adapter instances not seen and sessions not active for retentionDays go (a session a direct message still references stays); an active meeting without a message for meetingRetentionDays since its last message, start or reopening closes with endReason expired; a meeting closed or archived meetingRetentionDays ago goes with its messages, decisions, stances and members. With dryRun true it changes nothing and reports what it would do. 'export' returns meeting meetingId as markdown: state, members, decisions and messages with who wrote them and on which machine.",
3734
+ "description": "Host housekeeping. 'prune' removes the oldest count global chat messages (count ≥ 1 required), 'clear' removes all of them. 'cleanup' runs the hourly cleanup now: tasks past their TTL fail; unread direct messages and broadcast deliveries past their expiry expire; finished tasks, events, read or expired direct messages and broadcast deliveries (a broadcast with its last delivery), delivered notices, adapter instances not seen and sessions not active for retentionDays go (a session a direct message, a broadcast or a broadcast delivery still references stays); an active meeting without a message for meetingRetentionDays since its last message, start or reopening closes with endReason expired; a meeting closed or archived meetingRetentionDays ago goes with its messages, decisions, stances and members. With dryRun true it changes nothing and reports what it would do. 'export' returns meeting meetingId as markdown: state, members, decisions and messages with who wrote them and on which machine.",
3651
3735
  "annotations": {
3652
3736
  "readOnlyHint": false,
3653
3737
  "destructiveHint": true,
@@ -3708,7 +3792,7 @@
3708
3792
  "description": "prune and clear: global messages left."
3709
3793
  },
3710
3794
  "cleanup": {
3711
- "description": "cleanup: what was removed or changed, or with dryRun would be. counts: tasksExpired (failed for their TTL), tasks, events, meetingsExpired (closed), meetings (deleted), messagesExpired (unread direct messages marked expired), messages, notices, toolUseBindings, sessions, instances.",
3795
+ "description": "cleanup: what was removed or changed, or with dryRun would be. counts: tasksExpired (failed for their TTL), tasks, events, meetingsExpired (closed), meetings (deleted), messagesExpired (unread direct messages and broadcast deliveries marked expired), messages (direct messages and broadcast deliveries deleted), notices, toolUseBindings, sessions, instances.",
3712
3796
  "type": "object",
3713
3797
  "properties": {
3714
3798
  "dryRun": {
@@ -117,7 +117,7 @@ The wait never delivers while the session is `working` or while a listening Stop
117
117
  All data lives in one SQLite file, `nexus.db`, in the host's data directory, next to `settings.json` and `host.lock` (locations per OS: [User guide](USER_GUIDE.md#data-and-logs)).
118
118
 
119
119
  - **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 whose version is above the host's is refused, with a message to upgrade the host or, when the release says so, to delete `nexus.db`. Each file holds the whole current schema of one owner (`0001_core.sql`: internal/core; `0002_agents.sql`: internal/agents); a later schema change adds a numbered file.
120
- - **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`, `is_default` and the user's seat), `meeting_messages` (numbered per meeting with `seq`), `meeting_members` (who takes part, how, and what they were told), `decisions`, `global_messages`, `events`, `tasks`, `read_cursors` (what each reader has read of global chat and each meeting, and whether its last post to a meeting was refused for unread messages), and for conversations `agent_sessions`, `agent_messages`, `agent_notices`, `agent_idle_subscriptions`, `tool_use_bindings` and `hooked_instances` (adapter instances whose calls resolved a conversation, for chat and meeting identity).
120
+ - **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`, `is_default` and the user's seat), `meeting_messages` (numbered per meeting with `seq`), `meeting_members` (who takes part, how, and what they were told), `decisions`, `global_messages`, `events`, `tasks`, `read_cursors` (what each reader has read of global chat and each meeting, and whether its last post to a meeting was refused for unread messages), and for conversations `agent_sessions`, `agent_messages`, `broadcasts` and `broadcast_deliveries` (one row per broadcast and per recipient; the `agent_inbox` view joins direct messages and deliveries into one inbox), `agent_notices`, `agent_idle_subscriptions`, `tool_use_bindings` and `hooked_instances` (adapter instances whose calls resolved a conversation, for chat and meeting identity).
121
121
  - **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.
122
122
  - **Only the host opens the database**. MCP adapters and hooks go through HTTP.
123
123
  - **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.
@@ -129,8 +129,8 @@ Meeting members and stances store their resolved identity kind; draft eligibilit
129
129
  At startup and then every hour the host cleans up, with the two retention settings of `settings.json` ([User guide](USER_GUIDE.md#settingsjson)), `retentionDays` (default 7) and `meetingRetentionDays` (default 30):
130
130
 
131
131
  - marks pending or running tasks past their TTL as `failed`, and deletes finished tasks (`completed`, `failed`, `cancelled`) and events `retentionDays` after;
132
- - expires unread direct messages past their 24-hour lifetime (telling the senders), and deletes read or expired direct messages and delivered notices `retentionDays` after;
133
- - deletes sessions not active for `retentionDays`, with their notices, idle subscriptions, tool-use bindings and read cursors, unless a direct message still references them or they hold a wait; adapter instances not seen for `retentionDays` are forgotten;
132
+ - expires unread direct messages and broadcast deliveries past their 24-hour lifetime (telling the senders of direct messages), and deletes read or expired direct messages and broadcast deliveries and delivered notices `retentionDays` after, and a broadcast with its last delivery;
133
+ - deletes sessions not active for `retentionDays`, with their notices, idle subscriptions, tool-use bindings and read cursors, unless a direct message, a broadcast or a broadcast delivery still references them or they hold a wait; adapter instances not seen for `retentionDays` are forgotten;
134
134
  - closes an active meeting without a message for `meetingRetentionDays` (counted from its last message, its start or its reopening) with `endReason: expired`, telling its members; it makes no decision;
135
135
  - deletes meetings closed or archived `meetingRetentionDays` ago, with their messages, decisions, stances, members, approvers and read cursors;
136
136
  - deletes tool-use bindings once they have expired (5 minutes).
@@ -116,7 +116,7 @@ Stop ◀── decision: block ◀─┘ 每次结
116
116
  全部数据都在 host 数据目录里的一个 SQLite 文件 `nexus.db` 中,同一目录下还有 `settings.json` 和 `host.lock`(各系统的位置见[使用手册](USER_GUIDE_zh.md#数据和日志))。
117
117
 
118
118
  - **表结构**:嵌入 host 的编号迁移脚本(`host/internal/store/migrations/0001_core.sql`、`0002_agents.sql`)在启动时按序执行,每个文件一个事务;版本号存在 `PRAGMA user_version`。版本高于 host 的数据库会被拒绝,提示升级 host,或在版本说明要求时删除 `nexus.db`。每个文件包含一个模块当前的完整表结构(`0001_core.sql`:internal/core;`0002_agents.sql`:internal/agents);以后改表结构时新增编号文件。
119
- - **表**:`projects`(manifest 的标量字段各占一列,结构化字段存 JSON,内部文档存文本)、`project_relations` 和 `project_assets`(资产内容存 BLOB;两者都随项目改名、删除级联)、`global_strategy`、`global_docs`、`meetings`(带 `status`、`is_default` 和用户席位)、`meeting_messages`(用 `seq` 按会议编号)、`meeting_members`(谁参加、以什么方式、已提醒到哪里)、`decisions`、`global_messages`、`events`、`tasks`、`read_cursors`(每个读者在全局聊天和各会议里读到哪里,以及它上一次在会议里发言是否因未读被拒),以及对话用的 `agent_sessions`、`agent_messages`、`agent_notices`、`agent_idle_subscriptions`、`tool_use_bindings`,以及 `hooked_instances`(调用时解析出过对话的适配器实例,用于聊天和会议的身份识别)。
119
+ - **表**:`projects`(manifest 的标量字段各占一列,结构化字段存 JSON,内部文档存文本)、`project_relations` 和 `project_assets`(资产内容存 BLOB;两者都随项目改名、删除级联)、`global_strategy`、`global_docs`、`meetings`(带 `status`、`is_default` 和用户席位)、`meeting_messages`(用 `seq` 按会议编号)、`meeting_members`(谁参加、以什么方式、已提醒到哪里)、`decisions`、`global_messages`、`events`、`tasks`、`read_cursors`(每个读者在全局聊天和各会议里读到哪里,以及它上一次在会议里发言是否因未读被拒),以及对话用的 `agent_sessions`、`agent_messages`、`broadcasts` 和 `broadcast_deliveries`(每次广播一行、每个收件方一行;`agent_inbox` 视图把直接消息和送达记录合成一个收件箱)、`agent_notices`、`agent_idle_subscriptions`、`tool_use_bindings`,以及 `hooked_instances`(调用时解析出过对话的适配器实例,用于聊天和会议的身份识别)。
120
120
  - **事务**:每个写操作一个事务,多行修改(会议消息,开始 / 结束 / 重开会议,项目改名)要么全部生效,要么都不生效。数据库使用 WAL 模式,开启 `foreign_keys`,忙等超时 5 秒,只用一个连接。
121
121
  - **只有 host 打开数据库**。MCP 适配器和 hooks 都走 HTTP。
122
122
  - **活动日志**:通过工具产生的状态变更(项目同步 / 更新 / 改名 / 删除 / 资产、文档同步、策略更新、会议开始 / 结束 / 归档 / 重开、维护)记录为结构化事件,用 `list_events` 读取,不混入聊天流。
@@ -128,8 +128,8 @@ Stop ◀── decision: block ◀─┘ 每次结
128
128
  启动时一次,之后每小时一次,host 按 `settings.json` 的两个保留期设置([使用手册](USER_GUIDE_zh.md#settingsjson))清理:`retentionDays`(默认 7)和 `meetingRetentionDays`(默认 30):
129
129
 
130
130
  - 把超过 TTL 的待处理或运行中任务标为 `failed`,删除结束(`completed`、`failed`、`cancelled`)`retentionDays` 天后的任务和 `retentionDays` 天前的事件;
131
- - 让超过 24 小时有效期的未读直接消息过期(通知发送方),删除 `retentionDays` 天前已读或已过期的直接消息和已发出的通知;
132
- - 删除 `retentionDays` 天没有活动的会话,连同它的通知、空闲订阅、工具调用绑定和读取游标;仍被直接消息引用或正挂着等待的会话不删;`retentionDays` 天没出现的适配器实例不再记录;
131
+ - 让超过 24 小时有效期的未读直接消息和广播送达记录过期(直接消息通知发送方),删除 `retentionDays` 天前已读或已过期的直接消息、广播送达记录和已发出的通知,广播随最后一条送达记录删除;
132
+ - 删除 `retentionDays` 天没有活动的会话,连同它的通知、空闲订阅、工具调用绑定和读取游标;仍被直接消息、广播或广播送达记录引用,或正挂着等待的会话不删;`retentionDays` 天没出现的适配器实例不再记录;
133
133
  - 连续 `meetingRetentionDays` 天没有发言(从最后一条消息、开始或重新开启算起)的进行中会议以 `endReason: expired` 关闭并通知成员,不产生决议;
134
134
  - 删除关闭或归档 `meetingRetentionDays` 天的会议,连同消息、决议、表态、成员、认可人和读取游标;
135
135
  - 删除已过期(5 分钟)的工具调用绑定。
@@ -63,14 +63,15 @@ In Claude Code and Codex with the n2n-nexus hooks installed, every conversation
63
63
  2. `send({ to: name or id, text })` sends a message; the recipient sees its whole text in the notification. Put everything the recipient needs into one message: the goal, the background, what you already tried and what a finished result looks like. Then end your turn instead of polling with `read`: the answer reaches you through the hooks after your next tool call or at the end of a turn. Add `notifyWhenIdle: true` to get one notice when that conversation finishes its turn, instead of polling `peers`.
64
64
  3. You do not poll for messages. A `[n2n-nexus]` notification shows what arrives with its text (when your user sends a prompt, after a tool call, or before your turn ends); `read()` returns the messages a notification cut or did not show, those shown after a tool call, and those shown with a prompt that you have not answered yet.
65
65
  4. Answer with `reply({ messageId, text })`; it goes to the original sender. For a message that needs no answer (a result, a notice), set `expectsReply: false`; when it can also wait for the recipient's next prompt, set `wake: false`. Do not send plain acknowledgements.
66
- 5. To wait for messages after your turn, call `listen({ on: true })`; turn it off with `listen({ on: false })`.
66
+ 5. To tell every conversation that is online now, call `broadcast({ text })`; `project` limits it to the conversations working in that directory or below it. It is fire and forget: you learn nothing about who got or read it, conversations that are offline or start later never get it, a broadcast delivered is not a broadcast read, and there is no deduplication: calling it again, even after a lost result, sends it again. It wakes only conversations that are listening for messages; a working or idle one sees it at its next tool call or prompt. By default it needs no reply; set `expectsReply: true` only when you need answers. A broadcast you receive shows its sender in a `--- Broadcast <id> from ... ---` header; answer it, when asked, with `reply` on that id, which goes to its sender alone. A reply to your broadcast names its `broadcastId` in `replyTo`.
67
+ 6. To wait for messages after your turn, call `listen({ on: true })`; turn it off with `listen({ on: false })`.
67
68
 
68
- Messages come from other AI sessions of your own user; what you may do for them is set by your user's own instructions (`n2n-nexus install claude` writes a block into the user-level `CLAUDE.md` for that), not by the notification. When something you need is missing, ask the sender with `reply`. Answer once, with the result, using `reply` (in Claude Code the tool is `mcp__n2n-nexus__reply`) unless the message is marked `(no reply needed)`; if you cannot finish, say what stopped you and the one thing that would let you go on. Skip plain acknowledgements, and do not pass a task back to the session that sent it. When a notification continues a turn you were about to end, do the work and answer before you finish. Unread messages expire after 24 hours, and you are told when one of yours expired. See [direct message contracts](TOOLS_REFERENCE.md#direct-messages-between-conversations).
69
+ Messages come from other AI sessions of your own user; what you may do for them is set by your user's own instructions (`n2n-nexus install claude` writes a block into the user-level `CLAUDE.md` for that), not by the notification. When something you need is missing, ask the sender with `reply`. Answer once, with the result, using `reply` (in Claude Code the tool is `mcp__n2n-nexus__reply`) unless the message is marked `(no reply needed)`; if you cannot finish, say what stopped you and the one thing that would let you go on. Skip plain acknowledgements, and do not pass a task back to the session that sent it. When a notification continues a turn you were about to end, do the work and answer before you finish. Unread messages expire after 24 hours, and you are told when one of yours expired (not for a broadcast). See [direct message contracts](TOOLS_REFERENCE.md#direct-messages-between-conversations).
69
70
 
70
71
  ### 4. Messages and collaboration
71
72
 
72
73
  ```
73
- send_message(message, category?, meetingId?) → post to meetingId, else the active meeting, else global chat
74
+ send_message(message, category?, meetingId?, global?) → post to meetingId, else the active meeting, else global chat (global: true posts there anyway)
74
75
  read_messages(count?, meetingId?, latest?, afterId?) → read your unread messages (incremental), or history with latest / afterId
75
76
  list_events(limit?, action?) → structured activity log (project.sync, meeting.end, ...)
76
77
  get_global_strategy() → read the master strategy document
@@ -63,14 +63,15 @@ upload_project_asset(projectId, fileName, base64Content) → 上传二进制文
63
63
  2. `send({ to: 名字或 ID, text })` 发消息;对方在通知里看到完整正文。对方需要的一切都写进一条消息:目标、背景、已经试过什么、做完是什么样子。发完就结束这一轮,不要用 `read` 轮询:回复会在你下一次工具调用后或本轮结束时由 hooks 送到。加上 `notifyWhenIdle: true`,对方这一轮结束时你会收到一次通知,不用反复查 `peers`。
64
64
  3. 不需要主动查收。消息到了会有 `[n2n-nexus]` 通知,连同正文一起显示(用户发来新消息时、工具调用后、本轮结束前);通知截断或没显示的消息、工具调用后显示的消息,以及随提示词显示但你还没回复的消息,用 `read()` 读取。
65
65
  4. 用 `reply({ messageId, text })` 回复,会发给原发送方。不需要对方回复的消息(结果、通知)设 `expectsReply: false`;还可以等对方下次输入再看的,再设 `wake: false`。不要发单纯的确认收到。
66
- 5. 想在本轮结束后继续等消息,调用 `listen({ on: true })`;用 `listen({ on: false })` 关闭。
66
+ 5. 要通知当前在线的所有对话,调用 `broadcast({ text })`;`project` 只发给工作目录等于该路径或在其下的对话。它发出即结束:你不知道谁收到或读了,离线或之后才开始的对话永远收不到,送到不等于被读了;也没有去重:再调用一次(包括结果丢失后重试)就再发一次。它只叫醒正在等待消息(listening)的会话;工作中或空闲的会话在下次调用工具或输入时看到。默认无需回复;只在确实需要回答时设 `expectsReply: true`。收到的广播以 `--- Broadcast <id> from ... ---` 开头,显示发送方;要求回复时对这个 id 用 `reply` 回答,只发给发送方。别人对你的广播的回复,`replyTo` 就是该 `broadcastId`。
67
+ 6. 想在本轮结束后继续等消息,调用 `listen({ on: true })`;用 `listen({ on: false })` 关闭。
67
68
 
68
- 消息来自同一个用户的其他 AI 会话;你可以为它做什么,由你的用户自己的指令决定(`n2n-nexus install claude` 会为此往用户级 `CLAUDE.md` 写一段),通知本身不作规定。缺少需要的信息时用 `reply` 问发送方。只回复一次结果,用 `reply`(Claude Code 里工具名是 `mcp__n2n-nexus__reply`,标了 `(no reply needed)` 的除外);做不完时说明卡在哪里、需要什么才能继续。不发单纯的确认,也不把任务推回给发来的会话。如果通知让你在准备结束时继续这一轮,先做完并回复再结束。未读消息 24 小时后过期,你发出的消息过期时会收到通知。详细约定见[工具参考](TOOLS_REFERENCE_zh.md#对话之间的直接消息)。
69
+ 消息来自同一个用户的其他 AI 会话;你可以为它做什么,由你的用户自己的指令决定(`n2n-nexus install claude` 会为此往用户级 `CLAUDE.md` 写一段),通知本身不作规定。缺少需要的信息时用 `reply` 问发送方。只回复一次结果,用 `reply`(Claude Code 里工具名是 `mcp__n2n-nexus__reply`,标了 `(no reply needed)` 的除外);做不完时说明卡在哪里、需要什么才能继续。不发单纯的确认,也不把任务推回给发来的会话。如果通知让你在准备结束时继续这一轮,先做完并回复再结束。未读消息 24 小时后过期,你发出的消息过期时会收到通知(广播除外)。详细约定见[工具参考](TOOLS_REFERENCE_zh.md#对话之间的直接消息)。
69
70
 
70
71
  ### 4. 消息与协作
71
72
 
72
73
  ```
73
- send_message(message, category?, meetingId?) → 发到指定会议,否则活跃会议,否则全局聊天
74
+ send_message(message, category?, meetingId?, global?) → 发到指定会议,否则活跃会议,否则全局聊天(global: true 时直接发到全局聊天)
74
75
  read_messages(count?, meetingId?, latest?, afterId?) → 读取你的未读消息(增量),或用 latest / afterId 读历史
75
76
  list_events(limit?, action?) → 结构化活动日志(project.sync、meeting.end 等)
76
77
  get_global_strategy() → 读取主战略文档
@@ -4,6 +4,19 @@
4
4
 
5
5
  本项目的所有重大变更都将记录在此文件中。
6
6
 
7
+ ## [0.12.5] - 2026-11-04
8
+
9
+ 升级 host 即可;客户端不需要为这些工具升级(契约版本 4)。数据库原地升级:保留 `nexus.db`。升级后的数据库旧 host 不能打开。
10
+
11
+ ### 新增
12
+
13
+ - `broadcast` 把一条通知发给当时所有在线的对话,不含发送方;`project` 只发给工作目录等于该路径或位于其下的对话。它发出即结束:收件方由 host 选定并送达,发送方只拿到 `broadcastId`,不知道谁收到或读了,离线或之后才开始的对话永远收不到。host 只存一份,每个收件方一条送达记录,所有收件方看到的都是同一个 id,以 `--- Broadcast <id> from <发送方> ... ---` 开头。正在等待消息的收件方立即被叫醒看到它;工作中的不被打断(本轮结束时不拦住),在下次工具调用后看到;空闲的在下次输入或调用工具时看到。除非发送方设了 `expectsReply`,否则无需回复;对这个 id 的 `reply` 只发给发送方,`replyTo` 就是 `broadcastId`。未读的广播和直接消息一样 24 小时后过期,过期不通知任何人。`read` 的每条消息新增 `kind`(`direct` 或 `broadcast`)、`scope` 和 `replyToKind`。
14
+ - `send_message` 新增 `global: true`,有活跃会议时也能发到全局聊天;此前不带 `meetingId` 的消息总是发到活跃会议。
15
+
16
+ ### 变更
17
+
18
+ - `host_maintenance` 的清理也统计广播送达记录:`messagesExpired` 包括它标为过期的送达记录,`messages` 包括它删除的送达记录。
19
+
7
20
  ## [0.12.4] - 2026-10-30
8
21
 
9
22
  ### 修复
package/docs/README_zh.md CHANGED
@@ -108,7 +108,7 @@ host 提供 36 个工具,按用途分组:
108
108
  | **项目和资产** | `search_projects`、`get_project`、`update_project`、`rename_project`、`sync_project_assets`、`upload_project_asset` | 查找、读取、更新和重命名项目;同步 Manifest 和内部文档;上传二进制资产。 |
109
109
  | **全局文档** | `list_global_docs`、`get_global_doc`、`sync_global_doc`、`get_global_strategy`、`update_global_strategy` | 列出、读取、创建和更新共享的全局文档和主策略文档。 |
110
110
  | **消息** | `send_message`、`read_messages`、`list_events` | 发送会议或全局消息;读取消息和活动日志。 |
111
- | **直接消息** | `peers`、`send`、`read`、`reply`、`listen`、`set_profile` | 查找其他 Claude Code / Codex 对话,给它们发消息,读取并回复收到的消息,等待消息,给自己起名。 |
111
+ | **直接消息** | `peers`、`send`、`read`、`reply`、`broadcast`、`listen`、`set_profile` | 查找其他 Claude Code / Codex 对话,给它们发消息,读取并回复收到的消息,通知所有在线对话,等待消息,给自己起名。 |
112
112
  | **会议** | `list_meetings`、`start_meeting`、`join_meeting`、`leave_meeting`、`stance`、`end_meeting`、`archive_meeting`、`reopen_meeting` | 列出和开启会议;加入(有新消息会被叫醒)或退出;对当前草案认可或反对,全部认可即以共识结案,有用户席位时由用户决定;关闭、归档和重新打开会议。 |
113
113
  | **任务** | `create_task`、`get_task`、`list_tasks`、`update_task`、`cancel_task` | 创建、更新和跟踪由助手自行管理的任务。 |
114
114
  | **维护** | `host_maintenance`、`host_delete_project` | 清理或清空全局聊天流;立即按保留期清理;把会议导出为 markdown;删除项目。 |
@@ -11,7 +11,7 @@ Parameter tables below are derived from `contract/tools.json`, the JSON Schema t
11
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
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`, `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.
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`, `reply` and `broadcast` 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
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 hash also covers the WSL distribution, since distributions on one Windows machine share its host name, and 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
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).
@@ -143,22 +143,23 @@ Behavior:
143
143
 
144
144
  Every Claude Code and Codex conversation is one session with ID `claude:<session id>` or `codex:<session id>`, taken from the client. The hooks that `n2n-nexus install <claude|codex>` writes create the session and keep its status current; a sub-agent shares its parent conversation's session. Resuming a conversation keeps its session. A new conversation in the same client, such as after `/clear` in Claude Code, is a new session; Claude Code also ends the old one, which goes offline.
145
145
 
146
- The six tools below identify their caller from the MCP request's `_meta`, never from arguments: Claude Code attaches `claudecode/toolUseId`, which its PreToolUse hook bound to the conversation just before the call; Codex attaches `sessionId`. Other clients, conversations without the hooks, or a binding older than five minutes get an error that says direct messaging needs Claude Code or Codex with n2n-nexus hooks installed (`n2n-nexus install <claude|codex>`), that hooks apply to conversations started after they were installed, that the MCP server must be registered under the name `n2n-nexus`, and that an expired PreToolUse binding is fixed by calling the tool again. Every other tool ignores the caller.
146
+ The seven tools below identify their caller from the MCP request's `_meta`, never from arguments: Claude Code attaches `claudecode/toolUseId`, which its PreToolUse hook bound to the conversation just before the call; Codex attaches `sessionId`. Other clients, conversations without the hooks, or a binding older than five minutes get an error that says direct messaging needs Claude Code or Codex with n2n-nexus hooks installed (`n2n-nexus install <claude|codex>`), that hooks apply to conversations started after they were installed, that the MCP server must be registered under the name `n2n-nexus`, and that an expired PreToolUse binding is fixed by calling the tool again. Every other tool ignores the caller.
147
147
 
148
- These six tools are direct messages between conversations. Global chat and meetings are separate: `send_message` posts there and `read_messages` reads there; neither reaches or reads a conversation's direct messages.
148
+ These seven tools are direct messages between conversations. Global chat and meetings are separate: `send_message` posts there and `read_messages` reads there; neither reaches or reads a conversation's direct messages.
149
149
 
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
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 (its ID is in the notification and in `read`); the reply goes to its sender with `replyTo` set to it, and the message you answer is marked read. |
154
+ | `read` | `count?: integer = 10` (1–50) | `{ messages: [{ messageId, kind, from: { id, name }, scope, replyTo, replyToKind, text, createdAt, expectsReply }], remaining }`; unread messages and broadcasts oldest first, marked read. `kind` is `direct` or `broadcast`; for a broadcast `messageId` is its `broadcastId` and `scope` the directory it was limited to (`null` for every online session, and for a direct message). `replyTo` is the message or broadcast this one answers and `replyToKind` says which (`direct` or `broadcast`); both `null` when it answers nothing. |
155
+ | `reply` | `messageId: string`, `text: string` (1–32000 chars), `expectsReply?: boolean = true`, `wake?: boolean = true` | Same result as `send`. `messageId` is a direct message or a broadcast you received (its ID is in the notification and in `read`); the reply goes to its sender as a direct message with `replyTo` set to it, and the message or broadcast you answer is marked read. |
156
+ | `broadcast` | `text: string` (1–32000 chars), `project?: string` (1–1000 chars), `expectsReply?: boolean = false` | `{ broadcastId }`. A notice to every session that is not offline at the moment of the call, yourself excepted; with `project`, only the sessions whose working directory is that path or below it (whole path components, `/` or `\`, compared as written, so the same repository under WSL and Windows does not match). It is fire and forget, like a network broadcast: the host picks the recipients and delivers it, and the sender learns nothing about who got or read it, not even that nobody was online; a broadcast delivered to a session is not a broadcast read, and it may expire unread. Offline sessions and sessions that start later never get it. The host stores the broadcast once, with one delivery per recipient; every recipient sees it under the same ID, the `broadcastId`, as a message of kind `broadcast` from the sender. A recipient that is `listening` (in listen mode or in a background wait) is woken with it at once; a `working` one is not interrupted (its turn is not held at its end) and sees it after its next tool call, and an `idle` one at its next prompt or tool call. There is no `wake` argument. By default it needs no reply; with `expectsReply: true` each recipient is asked to answer with `reply` on the `broadcastId`, which reaches the sender alone as a direct message whose `replyTo` is that `broadcastId`. Calling it again sends it again: there is no deduplication, so a call whose result was lost may send the same broadcast twice. |
156
157
  | `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
158
  | `set_profile` | `name?: string` (1–48 of `A–Z a–z 0–9 . _ @ # -`), `note?: string` (≤ 200 chars), `title?: string` (≤ 80 chars), `label?: string` (≤ 32 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 `""`. A `label` names the machine you run on (such as `mac-air`) and is shown in place of the hostname; `""` removes it. |
158
159
 
159
160
  `Profile` contains `id`, `name`, `title`, `client` (`claude` or `codex`), `cwd`, `hostname`, `platform`, `environment`, `utcOffset`, `label`, `note`, `status` and `lastActiveAt`. `hostname` (on macOS the computer name set in System Settings, elsewhere the host name without a `.local` suffix), `platform` (`darwin`, `linux`, `wsl` for a Linux distribution under Windows, or `win32`), `environment` (the WSL distribution, which tells distributions on one Windows machine apart, else `""`) and `utcOffset` (local time minus UTC, in minutes) are what the session's hooks report with every event. Introductions, notifications and meeting messages show the machine as `<label or hostname> · <macOS | Linux | WSL | Windows>[ <environment>]` after the name, for example `claude@_projects#3f9a (Vector · WSL Ubuntu_26)`. `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 named `<client>@<working directory name>#<the last 4 letters and digits of its client session id>`, with more of the id when another session in the database (of any status) already has that name, so a name never stands for two sessions while both are recorded; a session that comes back after another took its name with `set_profile` is named again by the same rule. `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
161
 
161
- Delivery. The recipient's hooks announce new messages with their whole text: each listed message between `--- Message <id> from <sender> (<session id>) at <time> ---` and `--- End of message <id> ---`, preceded by the message it answers, and followed by how to answer it. The texts of one notification together are cut at 5000 characters (`notifyTextChars`); a cut message is marked and `read` returns it whole. The notification states facts only: the sender, the text, whether a reply is wanted and how to send it (for a Claude Code session also the tool's full name, `mcp__n2n-nexus__reply`); what the recipient may do for a message is set by its user's own instructions, such as the `CLAUDE.md` block that `install claude` writes. A message shown whole is marked read when the announcement continues or starts a turn (turn about to end, listening, idle), and, if it needs no reply, when a prompt shows it. Every other message stays unread until `reply` answers it or `read` returns it: one that asks for a reply shown with a prompt, and every message shown after a tool call, because that context can reach a sub-agent only; the end of the turn still holds such a message once. 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, each message with its text (see [`join_meeting`](#join_meeting)):
162
+ Delivery. The recipient's hooks announce new messages with their whole text: each listed message between `--- Message <id> from <sender> (<session id>) at <time> ---` (for a broadcast `--- Broadcast <broadcast id> from <sender> (<session id>) at <time>, sent to <every online session | the online sessions working in <project>> ---`) and `--- End of message <id> ---`, preceded by the message it answers (`This answers your broadcast <broadcast id>.` for a reply to a broadcast), and followed by how to answer it. The texts of one notification together are cut at 5000 characters (`notifyTextChars`); a cut message is marked and `read` returns it whole. The notification states facts only: the sender, the text, whether a reply is wanted and how to send it (for a Claude Code session also the tool's full name, `mcp__n2n-nexus__reply`); what the recipient may do for a message is set by its user's own instructions, such as the `CLAUDE.md` block that `install claude` writes. A message shown whole is marked read when the announcement continues or starts a turn (turn about to end, listening, idle), and, if it needs no reply, when a prompt shows it. Every other message stays unread until `reply` answers it or `read` returns it: one that asks for a reply shown with a prompt, and every message shown after a tool call, because that context can reach a sub-agent only; the end of the turn still holds such a message once. 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, each message with its text (see [`join_meeting`](#join_meeting)):
162
163
 
163
164
  | Client event | What the recipient sees |
164
165
  | --- | --- |
@@ -170,7 +171,7 @@ Delivery. The recipient's hooks announce new messages with their whole text: eac
170
171
  | 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
172
  | 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
173
 
173
- Unread messages expire after 24 hours; the sender then gets a notice. Notices also report `notifyWhenIdle` results.
174
+ Unread messages expire after 24 hours; the sender then gets a notice. An unread broadcast expires after 24 hours too, and nobody is told. Notices also report `notifyWhenIdle` results.
174
175
 
175
176
  HTTP clients: hooks call `POST /api/hook` with `{ client: "claude" | "codex", input }`, where `input` holds the hook's stdin fields `hook_event_name`, `session_id`, `cwd`, `stop_hook_active`, `tool_use_id`, `tool_name`, `source` and `reason`, plus `machine: { hostname, platform, environment, utcOffset }` (`schemas.hookMachine` in `contract/hook.json`), which every event of a conversation must carry: an event without a valid one is refused with 400; the response is `{ output }`, printed by the hook when not null. Tool calls name their session with `caller` beside `tool`, `args` and `instanceId` in `POST /api/tools/call`: `{ client: "claude", toolUseId }` or `{ client: "codex", sessionId }`. These identify conversations within the trusted local host; they are not credentials: any process of the same operating-system user can send them (see the local trust model in [SECURITY.md](../SECURITY.md)).
176
177
 
@@ -184,9 +185,11 @@ Post to a meeting or to global chat. For a message to one conversation, use `sen
184
185
  | `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
186
  | `meetingId` | string | no | Target meeting. Must be active. |
186
187
  | `draft` | boolean | no | Publish the message as the meeting's current draft (meetings only; the category is `PROPOSAL`). See [`stance`](#stance). |
188
+ | `global` | boolean | no | Post to global chat even while a meeting is active. Not with `meetingId` or `draft`. |
187
189
 
188
190
  Behavior:
189
191
 
192
+ - With `global: true`, the message goes to global chat.
190
193
  - With `meetingId`, the message goes to that meeting.
191
194
  - Otherwise, if a meeting is active, the message goes to it.
192
195
  - Otherwise it is written to global chat.
@@ -11,7 +11,7 @@
11
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
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`、`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` 不是只读的,因为它们会推进调用方自己的未读状态;但它们是非破坏性的,因为不改变任何共享数据。
14
+ - 每个工具都带有 MCP annotations:`readOnlyHint`、`destructiveHint`、`idempotentHint` 和 `openWorldHint: false`(所有工具都只作用于 host)。按照 MCP 的定义,只有更新完全是追加时,工具才算非破坏性。读取类工具标为只读。`start_meeting`、`join_meeting`、`create_task`、`send_message`、`send`、`reply` 和 `broadcast` 只追加数据,是非破坏性的(`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
15
  - 每个适配器都有一个实例 ID(`NEXUS_INSTANCE_ID`、`--id`,默认是 `mcp-<hostname>-<工作目录的哈希>-<IDE 名>`,其中哈希也包含 WSL 发行版名,因为同一台 Windows 上的发行版共用它的主机名;IDE 名是 MCP 客户端名,取自请求的 `io.modelcontextprotocol/clientInfo`,2025 代的客户端则取自 `initialize`)。同一个 IDE 在同一个工作区里重启后它保持不变,而同一个工作区里的两个 IDE 仍然互相区分。适配器在每次调用中都以 `instanceId` 带上它。同一个 IDE 在同一个工作区里的多个对话共用它,所以装了 n2n-nexus hooks 时,聊天和会议工具改为识别对话本身(见下一条);没装 hooks 的客户端按实例识别,这时没有 `instanceId` 的 `read_messages` 会失败。
16
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)中。
@@ -143,22 +143,23 @@
143
143
 
144
144
  每个 Claude Code 和 Codex 对话都是一个会话,ID 为 `claude:<会话 ID>` 或 `codex:<会话 ID>`,取自客户端。`n2n-nexus install <claude|codex>` 写入的 hooks 会创建会话并保持其状态最新;子代理与其父对话共用同一个会话。恢复对话时沿用原来的会话。在同一个客户端里开始的新对话(例如 Claude Code 中执行 `/clear` 之后)是一个新会话;Claude Code 还会结束旧会话,旧会话随之变为离线。
145
145
 
146
- 下面六个工具从 MCP 请求的 `_meta` 识别调用方,从不使用参数:Claude Code 带上 `claudecode/toolUseId`,它由 PreToolUse hook 在调用前刚刚绑定到对话;Codex 带上 `sessionId`。其他客户端、没有 hooks 的对话,或绑定已超过五分钟的调用会收到一条错误,说明:直接消息需要装有 n2n-nexus hooks 的 Claude Code 或 Codex(`n2n-nexus install <claude|codex>`);hooks 只对安装之后开始的对话生效;MCP 服务器必须以 `n2n-nexus` 为名注册;PreToolUse 绑定过期时再调用一次工具即可解决。其他所有工具都不关心调用方。
146
+ 下面七个工具从 MCP 请求的 `_meta` 识别调用方,从不使用参数:Claude Code 带上 `claudecode/toolUseId`,它由 PreToolUse hook 在调用前刚刚绑定到对话;Codex 带上 `sessionId`。其他客户端、没有 hooks 的对话,或绑定已超过五分钟的调用会收到一条错误,说明:直接消息需要装有 n2n-nexus hooks 的 Claude Code 或 Codex(`n2n-nexus install <claude|codex>`);hooks 只对安装之后开始的对话生效;MCP 服务器必须以 `n2n-nexus` 为名注册;PreToolUse 绑定过期时再调用一次工具即可解决。其他所有工具都不关心调用方。
147
147
 
148
- 这六个工具是对话之间的直接消息。全局聊天和会议是另外的东西:`send_message` 发到那里,`read_messages` 从那里读取;两者都不会发到或读取对话的直接消息。
148
+ 这七个工具是对话之间的直接消息。全局聊天和会议是另外的东西:`send_message` 发到那里,`read_messages` 从那里读取;两者都不会发到或读取对话的直接消息。
149
149
 
150
150
  | 工具 | 参数 | 结果与行为 |
151
151
  | --- | --- | --- |
152
152
  | `peers` | `includeOffline?: boolean = false` | `{ self: Profile, peers: Profile[] }`;其他未离线的会话,最近活跃的在前。带 `includeOffline` 时,还包括过去 24 小时内活跃过的离线会话。 |
153
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` 相同。该消息必须是发给你的(它的 ID 在通知和 `read` 里);回复发给它的发送方,`replyTo` 设为该消息,被回复的消息标为已读。 |
154
+ | `read` | `count?: integer = 10`(1–50) | `{ messages: [{ messageId, kind, from: { id, name }, scope, replyTo, replyToKind, text, createdAt, expectsReply }], remaining }`;未读消息和广播,最早的在前,读取后标为已读。`kind` 为 `direct` 或 `broadcast`;广播的 `messageId` 就是它的 `broadcastId`,`scope` 是它限定的目录(发给所有在线会话时为 `null`,直接消息也为 `null`)。`replyTo` 是这条消息回复的消息或广播,`replyToKind` 说明是哪种(`direct` 或 `broadcast`);不是回复时两者都为 `null`。 |
155
+ | `reply` | `messageId: string`、`text: string`(1–32000 个字符)、`expectsReply?: boolean = true`、`wake?: boolean = true` | 结果与 `send` 相同。`messageId` 是你收到的直接消息或广播(ID 在通知和 `read` 里);回复作为直接消息发给它的发送方,`replyTo` 设为它,被回复的消息或广播标为已读。 |
156
+ | `broadcast` | `text: string`(1–32000 个字符)、`project?: string`(1–1000 个字符)、`expectsReply?: boolean = false` | `{ broadcastId }`。发给调用时所有未离线会话的通知,不含自己;带 `project` 时只发给工作目录等于该路径或位于其下的会话(按整段路径比较,分隔符 `/` 或 `\`,按原样比较,所以同一个仓库在 WSL 和 Windows 下的路径不算匹配)。它像网络广播一样发出即结束:收件方由 host 选定并送达,发送方不知道谁收到或读了,没有任何在线会话时也不知道;送到某个会话不等于被读了,它可能未读就过期。离线会话和之后才开始的会话永远收不到。host 只存一份广播,每个收件方一条送达记录;所有收件方看到的都是同一个 ID,即 `broadcastId`,作为来自发送方、`kind` 为 `broadcast` 的消息。处于 `listening`(listen 模式或后台等待)的收件方会立即被叫醒看到它;`working` 的不被打断(本轮结束时不被拦住),在下次工具调用后看到;`idle` 的在下次输入或调用工具时看到。没有 `wake` 参数。默认无需回复;`expectsReply: true` 时请每个收件方对这个 `broadcastId` 用 `reply` 回答,回复作为直接消息只发给发送方,其 `replyTo` 就是该 `broadcastId`。再调用一次就再发一次:没有去重,所以结果丢失的调用重试后可能发出两次同样的广播。 |
156
157
  | `listen` | `on: boolean` | `{ listening }`。开启期间,对话每结束一轮,其 Stop hook 会等待最多 590 秒,有消息或通知到达时让对话继续。在 Claude Code 中按 Esc 会结束一次等待。 |
157
158
  | `set_profile` | `name?: string`(1–48 个 `A–Z a–z 0–9 . _ @ # -` 中的字符)、`note?: string`(≤ 200 个字符)、`title?: string`(≤ 80 个字符)、`label?: string`(≤ 32 个字符);至少提供一个 | `{ profile }`。名称在未离线的会话中唯一。 自己设置的 `title` 会取代客户端提供的标题,直到设为 `""`。`label` 是你所在机器的名字(如 `mac-air`),显示时取代主机名;设为 `""` 删除。 |
158
159
 
159
160
  `Profile` 包含 `id`、`name`、`title`、`client`(`claude` 或 `codex`)、`cwd`、`hostname`、`platform`、`environment`、`utcOffset`、`label`、`note`、`status` 和 `lastActiveAt`。`hostname`(macOS 上是系统设置里起的电脑名,其他系统是去掉 `.local` 后缀的主机名)、`platform`(`darwin`、`linux`、`wsl` 表示 Windows 下的 Linux 发行版、`win32`)、`environment`(WSL 发行版名,用来区分同一台 Windows 上的多个发行版,其他情况为 `""`)和 `utcOffset`(本地时间减 UTC 的分钟数)由会话的 hooks 随每个事件上报。介绍、通知和会议消息在名字后面显示机器,格式为 `<label 或主机名> · <macOS | Linux | WSL | Windows>[ <environment>]`,例如 `claude@_projects#3f9a (Vector · WSL Ubuntu_26)`。`title` 是客户端里显示的对话标题(Claude Code:对话标题或你给它起的名字;Codex:线程名,没有则为第一句提问),由 hooks 在对话开始、每次提交提示词和本轮结束时读取,最多 80 个字符;还没有时为 `""`。它显示在 `peers`、介绍、通知和会议成员里名称的旁边;`send` 仍用 `name`。会话的名称为 `<client>@<工作目录名>#<客户端会话 ID 的最后 4 个字母和数字>`;数据库里已有别的会话(不论状态)用了这个名称时,多取 ID 的位数,所以只要两个会话的记录都在,一个名称就不会指向两个会话;会话恢复时如果名称已被别的会话用 `set_profile` 占用,就按同样的规则重新取名。`status` 为 `working`(用户发送了提示词或有工具在运行)、`idle`(本轮已结束;有后台等待消息的会话改为显示 `listening`)、`listening`(在 listen 模式下等待)或 `offline`(对话已结束)。客户端不一定会报告对话结束,所以 `idle` 或 `working` 的会话 30 分钟没有任何活动也会变为 `offline`;它的下一个事件会让它恢复。只有 `listening` 的会话会立即回应;`idle` 和 `working` 的会话要等它的用户下次发送提示词或它下次调用工具时才会看到消息。
160
161
 
161
- 送达。收件方的 hooks 提醒新消息时带上完整正文:每条列出的消息夹在 `--- Message <id> from <发送方> (<会话 ID>) at <时间> ---` 和 `--- End of message <id> ---` 之间,前面是它回复的消息,后面是怎么回复它。一条通知里所有正文合计截到 5000 个字符(`notifyTextChars`);被截断的消息会标出来,`read` 返回它的全文。通知只陈述事实:发送方、正文、要不要回复、怎么回复(Claude Code 会话还给出工具全名 `mcp__n2n-nexus__reply`);收件方可以为一条消息做什么,由它的用户自己的指令决定,例如 `install claude` 写进 `CLAUDE.md` 的那段。完整显示的消息在提醒让一轮继续或开始时(本轮即将结束、待命、空闲)标为已读,无需回复的消息随提示词显示时也标为已读。其他消息仍为未读,直到用 `reply` 回复或用 `read` 读取:随提示词显示、需要回复的消息,以及工具调用后显示的所有消息,因为那段上下文可能只到了子代理;这类消息在本轮结束时照样拦住一次。每条消息只列出一次;它一直未读时,之后的通知只计数。`expectsReply: false` 的消息会标上 `(no reply needed)`,消息全都无需回复的通知不会要求回复。已加入的会议的新消息也这样送达,每条带正文(见 [`join_meeting`](#join_meeting)):
162
+ 送达。收件方的 hooks 提醒新消息时带上完整正文:每条列出的消息夹在 `--- Message <id> from <发送方> (<会话 ID>) at <时间> ---`(广播为 `--- Broadcast <广播 ID> from <发送方> (<会话 ID>) at <时间>, sent to <every online session | the online sessions working in <project>> ---`)和 `--- End of message <id> ---` 之间,前面是它回复的消息(回复广播时为 `This answers your broadcast <广播 ID>.`),后面是怎么回复它。一条通知里所有正文合计截到 5000 个字符(`notifyTextChars`);被截断的消息会标出来,`read` 返回它的全文。通知只陈述事实:发送方、正文、要不要回复、怎么回复(Claude Code 会话还给出工具全名 `mcp__n2n-nexus__reply`);收件方可以为一条消息做什么,由它的用户自己的指令决定,例如 `install claude` 写进 `CLAUDE.md` 的那段。完整显示的消息在提醒让一轮继续或开始时(本轮即将结束、待命、空闲)标为已读,无需回复的消息随提示词显示时也标为已读。其他消息仍为未读,直到用 `reply` 回复或用 `read` 读取:随提示词显示、需要回复的消息,以及工具调用后显示的所有消息,因为那段上下文可能只到了子代理;这类消息在本轮结束时照样拦住一次。每条消息只列出一次;它一直未读时,之后的通知只计数。`expectsReply: false` 的消息会标上 `(no reply needed)`,消息全都无需回复的通知不会要求回复。已加入的会议的新消息也这样送达,每条带正文(见 [`join_meeting`](#join_meeting)):
162
163
 
163
164
  | 客户端事件 | 收件方看到的内容 |
164
165
  | --- | --- |
@@ -170,7 +171,7 @@
170
171
  | 空闲(本轮已结束) | 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
172
  | 自动轮次上限 | 被叫醒、待命中的 Stop,以及因私信或其通知而不结束的 Stop,各算一次自动轮次;会议新消息和会议通知不计数,也不受限制;提醒中会写 `Automatic turn n of N`。设置了 `maxAutoTurns` 时(host `settings.json`;默认 0,即不限制,提醒中也不写计数),从用户上次输入以来达到这个次数后,消息仍会保存,但不再叫醒或拦住对话;用户下次输入时提醒这些消息,并重新开始计数。叫醒后一分钟内的提示词视为被叫醒的那一轮开始,不清零计数。 |
172
173
 
173
- 未读消息 24 小时后过期,发送方随后会收到通知。通知也会报告 `notifyWhenIdle` 的结果。
174
+ 未读消息 24 小时后过期,发送方随后会收到通知。未读的广播也在 24 小时后过期,不通知任何人。通知也会报告 `notifyWhenIdle` 的结果。
174
175
 
175
176
  HTTP 客户端:hooks 调用 `POST /api/hook`,请求体为 `{ client: "claude" | "codex", input }`,其中 `input` 包含 hook 标准输入中的字段 `hook_event_name`、`session_id`、`cwd`、`stop_hook_active`、`tool_use_id`、`tool_name`、`source` 和 `reason`,另加 `machine: { hostname, platform, environment, utcOffset }`(见 `contract/hook.json` 的 `schemas.hookMachine`),对话的每个事件都必须带上,缺少或无效时返回 400;响应为 `{ output }`,不为 null 时由 hook 打印。工具调用在 `POST /api/tools/call` 中用与 `tool`、`args`、`instanceId` 并列的 `caller` 指明会话:`{ client: "claude", toolUseId }` 或 `{ client: "codex", sessionId }`。它们只在可信本地 host 范围内标识对话,不是凭证:同一系统用户的任何进程都能发送它们(见 [SECURITY_zh.md](SECURITY_zh.md) 中的本地信任模型)。
176
177
 
@@ -184,9 +185,11 @@ HTTP 客户端:hooks 调用 `POST /api/hook`,请求体为 `{ client: "claude
184
185
  | `category` | string | 否 | `PROPOSAL`、`DECISION`、`UPDATE`、`CHAT` 或 `MEETING_START`(保留值;host 从不写入)之一。不传时消息没有分类。 |
185
186
  | `meetingId` | string | 否 | 目标会议。必须处于活跃状态。 |
186
187
  | `draft` | boolean | 否 | 把这条消息发布为会议的当前待决稿(只限会议;类别为 `PROPOSAL`)。见 [`stance`](#stance)。 |
188
+ | `global` | boolean | 否 | 即使有活跃会议也发到全局聊天。不能与 `meetingId` 或 `draft` 同用。 |
187
189
 
188
190
  行为:
189
191
 
192
+ - `global: true` 时,消息发到全局聊天。
190
193
  - 提供 `meetingId` 时,消息发到该会议。
191
194
  - 否则,如果有活跃会议,消息发到该会议。
192
195
  - 否则写入全局聊天。
@@ -119,7 +119,7 @@ Read when the host starts; command-line flags override it. A missing file or fie
119
119
  | `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` |
120
120
  | `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` |
121
121
  | `meetingRetentionDays` | Days a meeting is kept after it was closed or archived (then it goes with its messages and decisions; `host_maintenance` `export` saves it as markdown first), and days without a message after which an active meeting is closed with `endReason: expired`. At least `1` | `30` |
122
- | `retentionDays` | Days kept: read or expired direct messages, delivered notices, events, finished tasks, sessions without activity (unless a direct message still references them) and adapter instances not seen. At least `1` | `7` |
122
+ | `retentionDays` | Days kept: read or expired direct messages and broadcast deliveries, delivered notices, events, finished tasks, sessions without activity (unless a direct message, a broadcast or a broadcast delivery still references them) and adapter instances not seen. At least `1` | `7` |
123
123
 
124
124
  After editing the file, restart the host (tray **Restart**, or restart `serve` or the service).
125
125
 
@@ -119,7 +119,7 @@ host 启动时读取;命令行参数优先。文件或字段缺失时使用默
119
119
  | `maxAutoTurns` | 两次用户输入之间,一个对话最多可以自动继续的轮数(包括被叫醒、listen 等待、因消息被拦下不结束);超过后私信只排队,等用户下次输入。会议新消息不计数也不受限制。`0` 表示不限制 | `0` |
120
120
  | `stanceReminderMinutes` | 会议当前稿还有必需认可人没表态时,会议可以安静多少分钟;超过后每个没表态的人被提醒一次,草案作者收到一份缺谁的汇总。提醒从不结束会议。`0` 关闭提醒 | `10` |
121
121
  | `meetingRetentionDays` | 会议关闭或归档后保留的天数(之后连同消息和决议一起删除;可先用 `host_maintenance` 的 `export` 存为 markdown),也是进行中会议连续没有发言、被以 `endReason: expired` 关闭的天数。至少 `1` | `30` |
122
- | `retentionDays` | 以下内容保留的天数:已读或已过期的直接消息、已发出的通知、事件、结束的任务、没有活动的会话(仍被直接消息引用的不删)和没再出现的适配器实例。至少 `1` | `7` |
122
+ | `retentionDays` | 以下内容保留的天数:已读或已过期的直接消息和广播送达记录、已发出的通知、事件、结束的任务、没有活动的会话(仍被直接消息、广播或广播送达记录引用的不删)和没再出现的适配器实例。至少 `1` | `7` |
123
123
 
124
124
  改完文件后重启 host(托盘 **重启**,或重启 `serve` / 服务)。
125
125
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n2n-nexus",
3
- "version": "0.12.4",
3
+ "version": "0.12.5",
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",
@@ -99,14 +99,17 @@
99
99
  "dependencies": {
100
100
  "@modelcontextprotocol/server": "^2.3.1"
101
101
  },
102
+ "overrides": {
103
+ "minimatch": "^11.0.4"
104
+ },
102
105
  "devDependencies": {
103
106
  "@eslint/js": "^10.0.1",
104
107
  "@modelcontextprotocol/client": "^2.3.1",
105
108
  "@types/node": "^26.6.5",
106
- "eslint": "^10.13.0",
109
+ "eslint": "^10.14.0",
107
110
  "lefthook": "^2.2.2",
108
111
  "typescript": "^6.0.3",
109
- "typescript-eslint": "^8.72.0",
112
+ "typescript-eslint": "^8.72.1",
110
113
  "vitest": "^5.0.5"
111
114
  }
112
115
  }