n2n-nexus 0.12.2 → 0.12.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,33 @@
4
4
 
5
5
  All notable changes to this project will be documented in this file.
6
6
 
7
+ ## [0.12.4] - 2026-10-30
8
+
9
+ ### Fixed
10
+
11
+ - On Windows, a Codex conversation is woken while idle again. The background wait copy that the Stop `--wait` hook starts inherited the pipes Codex reads the hook's output from (Codex runs the hook through PowerShell), so Codex waited for them until the hook's timeout and ended the copy with the hook: the session never showed as `listening` and only saw messages at its next prompt or tool call. The copy is now started by PowerShell's `Start-Process`, which passes no handles on.
12
+
13
+ ### Changed
14
+
15
+ - A wake the client reports as failed (`codex queue` returned an error or a non-zero exit) is logged in `host.log` when the host undoes it: the session, the wake and how many messages and notices are delivered again. Before, the undo and the retry left no trace.
16
+
17
+ ## [0.12.3] - 2026-10-29
18
+
19
+ ### Fixed
20
+
21
+ - A session whose Stop event was lost (the host was down or restarting when its turn ended) goes stale after 30 minutes like any other: the background wait no longer counts as its activity while it is working, so it stops being reported as working and messages sent to it stop waiting for a wake that never comes.
22
+ - A host bound to a name (`--host mybox`) accepts requests addressed by that name: its own health probes, the tray and `service install` no longer get 403 and report that nothing answers.
23
+ - Profile texts (`set_profile` title, note and label), the title a client reports, the host name, the WSL distribution and the working directory are stored on one line: line breaks and Unicode line separators in them can no longer forge a message's framing lines in another session's notification.
24
+ - A member that joins a meeting again after `leave_meeting` starts from the newest message, as documented, instead of being refused its first post for the messages it missed.
25
+ - A database written by an older host whose migrations fail now says to delete `nexus.db` when the CHANGELOG of the release says to reset the data directory, instead of a bare SQL error.
26
+ - The client survives an endpoint that is not an n2n-nexus host: an error status with a non-object body no longer crashes the hook or the MCP adapter, a tool list that is not a list leaves the adapter unconnected (it retries) instead of half-connected, and a tool call answered with a non-object body fails as a transport error. The discovery cache is written through a temporary file and renamed into place, so a concurrent hook never reads a partial URL.
27
+ - On Windows, upgrading through `install` restarts the background service with its data directory intact when that directory ends in a backslash and contains a space.
28
+
29
+ ### Changed
30
+
31
+ - The Windows firewall rules added by **Allow LAN access** apply to private and domain networks only, not to public ones. Rules added by an earlier version are not changed until LAN access is turned off and on again.
32
+ - On WSL, the MCP adapter's instance ID includes the distribution, since distributions on one Windows machine share its host name; two distributions on the same workspace no longer share chat and meeting read cursors. The ID changes once for WSL users, which resets those cursors (not the hooks' per-conversation ones).
33
+
7
34
  ## [0.12.2] - 2026-10-29
8
35
 
9
36
  ### Changed
package/build/cli/hook.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * is silent: exit 0 with nothing on stdout, so the client carries on unaffected.
8
8
  */
9
9
  import { spawn } from "node:child_process";
10
+ import path from "node:path";
10
11
  import { BACKGROUND_WAIT_EVENT, WAKE_RESULT_EVENT, HOOK_INPUT_FIELDS, HOOK_REQUEST_TIMEOUT_MS, HOOK_STOP_REQUEST_TIMEOUT_MS, SESSION_END_REQUEST_TIMEOUT_MS, WAIT_HOOK_TIMEOUT_SEC, WAKE_RETRY_FIRST_DELAY_MS, WAKE_RETRY_MAX_DELAY_MS, noDaemonHint, isHookClient } from "../contract.js";
11
12
  import { NexusClient, NexusTransportError, REQUEST_TIMEOUT_CODE } from "../client/nexus-client.js";
12
13
  import { DISCOVER_ENDPOINT, configuredEndpoint, resolveEndpoint } from "../client/discovery.js";
@@ -162,7 +163,7 @@ async function runWait(argv) {
162
163
  if (client === "codex") {
163
164
  const { entryScript } = await import("./entry.js");
164
165
  const args = [entryScript(), "hook", "codex", "--wait", "--session", sessionId, ...(argValue(argv, "--endpoint") ? ["--endpoint", configured] : [])];
165
- spawn(process.execPath, args, { detached: true, stdio: "ignore", windowsHide: true }).unref();
166
+ await startDetached(process.execPath, args);
166
167
  return 0;
167
168
  }
168
169
  }
@@ -189,6 +190,30 @@ async function runWait(argv) {
189
190
  await new Promise((resolve) => setTimeout(resolve, delay));
190
191
  }
191
192
  }
193
+ /**
194
+ * Start a program that outlives this hook, with no stdio. On Windows it is
195
+ * started by PowerShell's Start-Process (ShellExecute), which passes no handles
196
+ * on: a process Node spawns inherits every inheritable handle of this one, and
197
+ * when Codex runs the hook through PowerShell (`commandWindows`) those include
198
+ * the pipes Codex reads the hook's output from. Codex then waits for the pipes
199
+ * until the hook's timeout and ends the wait copy with the hook.
200
+ */
201
+ async function startDetached(file, args) {
202
+ if (process.platform !== "win32") {
203
+ spawn(file, args, { detached: true, stdio: "ignore", windowsHide: true }).unref();
204
+ return;
205
+ }
206
+ const literal = (s) => `'${s.replace(/'/g, "''")}'`;
207
+ // Start-Process joins -ArgumentList with spaces, so each argument is quoted for the command line itself.
208
+ const quoted = (s) => `"${s.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, "$1$1")}"`;
209
+ const command = `Start-Process -WindowStyle Hidden -FilePath ${literal(file)} -ArgumentList ${args.map((a) => literal(quoted(a))).join(",")}`;
210
+ const powershell = path.join(process.env.SystemRoot || "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
211
+ await new Promise((resolve, reject) => {
212
+ const launcher = spawn(powershell, ["-NoProfile", "-NonInteractive", "-Command", command], { stdio: "ignore", windowsHide: true });
213
+ launcher.on("error", reject);
214
+ launcher.on("exit", (code) => (code === 0 ? resolve() : reject(new Error(`Start-Process exited with ${code}`))));
215
+ });
216
+ }
192
217
  /** Run the hook command; resolves to exit code 0, or 2 when a background wait wakes a Claude conversation. */
193
218
  export async function runHook(argv) {
194
219
  if (argv.includes("--wait")) {
@@ -120,8 +120,18 @@ export async function resolveEndpoint(configured, options = {}) {
120
120
  return cached.endpoint;
121
121
  const endpoint = await discoverEndpoint() ?? LOCAL_ENDPOINT;
122
122
  try {
123
- fs.mkdirSync(path.dirname(cacheFile()), { recursive: true });
124
- fs.writeFileSync(cacheFile(), endpoint + "\n");
123
+ // Written next to the file and renamed over it: a hook reading it at the same time never sees a partial URL.
124
+ const target = cacheFile();
125
+ fs.mkdirSync(path.dirname(target), { recursive: true });
126
+ const tmp = `${target}.${process.pid}.tmp`;
127
+ fs.writeFileSync(tmp, endpoint + "\n");
128
+ try {
129
+ fs.renameSync(tmp, target);
130
+ }
131
+ catch (e) {
132
+ fs.rmSync(tmp, { force: true });
133
+ throw e;
134
+ }
125
135
  }
126
136
  catch { /* caching is best effort */ }
127
137
  return endpoint;
@@ -68,7 +68,9 @@ export class NexusClient {
68
68
  }
69
69
  const status = res.statusCode ?? 0;
70
70
  if (status >= 400) {
71
- reject(new NexusToolError(parsed.error || `HTTP ${status}`, status));
71
+ // The body may be any JSON value (null, a number, ...): only an object carries an error text.
72
+ const error = typeof parsed === "object" && parsed !== null ? parsed.error : undefined;
73
+ reject(new NexusToolError(typeof error === "string" && error ? error : `HTTP ${status}`, status));
72
74
  }
73
75
  else {
74
76
  resolve(parsed);
@@ -88,6 +90,8 @@ export class NexusClient {
88
90
  }
89
91
  async fetchTools() {
90
92
  const res = await this.request("GET", "/api/tools");
93
+ if (typeof res !== "object" || res === null || !Array.isArray(res.tools))
94
+ throw new NexusTransportError("Invalid tools response from n2n-nexus-host");
91
95
  return res.tools;
92
96
  }
93
97
  /** Forward a client hook event; `timeoutMs` overrides the client timeout (a listening Stop waits long). */
@@ -96,6 +100,8 @@ export class NexusClient {
96
100
  }
97
101
  async callTool(name, args, instanceId, caller) {
98
102
  const res = await this.request("POST", "/api/tools/call", { tool: name, args: args || {}, ...(instanceId ? { instanceId } : {}), ...(caller ? { caller } : {}) });
103
+ if (!res || typeof res !== "object")
104
+ throw new NexusTransportError("Invalid tool call response from n2n-nexus-host");
99
105
  if (!res.ok)
100
106
  throw new NexusToolError(res.error || "Tool call failed", 200);
101
107
  return res.result;
@@ -6,6 +6,8 @@ import os from "node:os";
6
6
  * survive an IDE restart: derive it from the machine, the workspace the IDE
7
7
  * launched the adapter in and the IDE itself (two IDEs open on the same
8
8
  * workspace must not share cursors), unless the user pins one explicitly.
9
+ * WSL distributions on one Windows machine share its host name, so the
10
+ * distribution goes into the workspace hash.
9
11
  */
10
12
  export function resolveInstanceId(env = process.env, argv = process.argv, cwd = process.cwd(), clientName) {
11
13
  if (env.NEXUS_INSTANCE_ID)
@@ -13,7 +15,7 @@ export function resolveInstanceId(env = process.env, argv = process.argv, cwd =
13
15
  const idx = argv.indexOf("--id");
14
16
  if (idx !== -1 && argv[idx + 1])
15
17
  return argv[idx + 1];
16
- const digest = createHash("sha1").update(cwd).digest("hex").slice(0, 8);
18
+ const digest = createHash("sha1").update(env.WSL_DISTRO_NAME ? `${env.WSL_DISTRO_NAME}\n${cwd}` : cwd).digest("hex").slice(0, 8);
17
19
  const client = clientName ? `-${clientName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 24)}` : "";
18
20
  return `mcp-${os.hostname()}-${digest}${client}`;
19
21
  }
@@ -92,7 +92,9 @@ turn ends ──▶ Stop hook (announces unread, or listens)
92
92
  │ session shows as "listening"
93
93
  ├─ new message ─▶ { wake: notification, dispatchId }
94
94
  │ Claude Code: asyncRewake hook exits 2, the notification starts a turn
95
- │ Codex: detached process runs `codex queue --thread <id> --message <notification>`,
95
+ │ Codex: detached process (on Windows started by PowerShell's Start-Process, which
96
+ │ passes no handles on: a process Node spawns would inherit the hook's output pipes
97
+ │ and Codex would wait for them) runs `codex queue --thread <id> --message <notification>`,
96
98
  │ then reports POST /api/hook {WakeResult, dispatch_id, ok}
97
99
  ├─ any other event of the session ─▶ null (wait ends)
98
100
  └─ time runs out or host shuts down ─▶ { timeout: true } (ask again)
@@ -100,7 +102,7 @@ turn ends ──▶ Stop hook (announces unread, or listens)
100
102
 
101
103
  The background wait retries temporary connection failures within its original one-hour deadline; with `discover`, it looks up the host again. A normal cancellation (`output: null`) still ends the wait. Host and client must both include this recovery behavior for graceful host restarts to resume waiting.
102
104
 
103
- The wait never delivers while the session is `working` or while a listening Stop hook is waiting, so a message is announced once. When `codex queue` fails (an error or a non-zero exit), the Codex wait reports `ok: false` and the host undoes that wake: the messages and notices it announced count as not announced again, the session leaves `working` and the automatic turn is not counted; the wait then asks again after 1 s, doubling after each failure up to 5 minutes, and delivers them again. Any other event of the session before the report means the wake was delivered, and the report is ignored. Verified on Windows with the Claude desktop app and the Codex desktop app (2026-10-23): both directions wake an idle conversation within seconds. Known limits:
105
+ The wait never delivers while the session is `working` or while a listening Stop hook is waiting, so a message is announced once. When `codex queue` fails (an error or a non-zero exit), the Codex wait reports `ok: false` and the host undoes that wake: the messages and notices it announced count as not announced again, the session leaves `working` and the automatic turn is not counted, and the failed wake is logged in `host.log` (the session, the wake and how many messages and notices are delivered again); the wait then asks again after 1 s, doubling after each failure up to 5 minutes, and delivers them again. Any other event of the session before the report means the wake was delivered, and the report is ignored. Verified on Windows with the Claude desktop app and the Codex desktop app (2026-10-23): both directions wake an idle conversation within seconds. Known limits:
104
106
 
105
107
  - Codex starts a queued turn only for a conversation loaded in the running Codex app. After Codex restarts, a conversation is loaded once the user opens it.
106
108
  - The Codex background wait ends when the Codex app quits, and starts again only when the conversation ends a turn. Messages sent meanwhile stay unread in the host (not in Codex's own queue) until the user next prompts that conversation.
@@ -92,15 +92,16 @@ Stop ◀── decision: block ◀─┘ 每次结
92
92
  │ 等待期间会话显示为 "listening"
93
93
  ├─ 有新消息 ─▶ { wake: 通知, dispatchId }
94
94
  │ Claude Code:asyncRewake hook 以退出码 2 结束,通知开启新的一轮
95
- │ Codex:独立后台进程执行 `codex queue --thread <id> --message <通知>`,
96
- │ 再回报 POST /api/hook {WakeResult, dispatch_id, ok}
95
+ │ Codex:独立后台进程(Windows 上由 PowerShell 的 Start-Process 启动,它不传递任何句柄:
96
+ │ Node 直接派生的进程会继承 hook 的输出管道,Codex 会一直等这些管道)执行
97
+ │ `codex queue --thread <id> --message <通知>`,再回报 POST /api/hook {WakeResult, dispatch_id, ok}
97
98
  ├─ 该会话有其他任何事件 ─▶ null(等待结束)
98
99
  └─ 时间到或 host 关闭 ─▶ { timeout: true }(再次请求)
99
100
  ```
100
101
 
101
102
  后台等待会在原有的一小时时限内重试临时连接故障;使用 `discover` 时重新查找 host。正常取消(`output: null`)仍结束等待。host 与客户端都需要包含这项恢复行为,才能在 host 正常重启后继续等待。
102
103
 
103
- 会话处于 `working`,或者 listen 模式的 Stop hook 正在等待时,后台等待不会送达,所以同一条消息只提醒一次。`codex queue` 失败(出错或退出码非 0)时,Codex 的后台等待回报 `ok: false`,host 撤销这次唤醒:它提醒过的消息和通知重新算作未提醒,会话退出 `working`,这一轮不计入自动轮次;随后等待在 1 秒后重新请求(每失败一次间隔加倍,最长 5 分钟),再次送达它们。回报到达前该会话已有其他事件,说明唤醒已送达,回报被忽略。已在 Windows 上用 Claude 桌面应用和 Codex 桌面应用实测(2026-10-23):两个方向都能在数秒内叫醒空闲的对话。已知限制:
104
+ 会话处于 `working`,或者 listen 模式的 Stop hook 正在等待时,后台等待不会送达,所以同一条消息只提醒一次。`codex queue` 失败(出错或退出码非 0)时,Codex 的后台等待回报 `ok: false`,host 撤销这次唤醒:它提醒过的消息和通知重新算作未提醒,会话退出 `working`,这一轮不计入自动轮次,失败的唤醒记入 `host.log`(会话、唤醒 ID、再次送达的消息和通知各有几条);随后等待在 1 秒后重新请求(每失败一次间隔加倍,最长 5 分钟),再次送达它们。回报到达前该会话已有其他事件,说明唤醒已送达,回报被忽略。已在 Windows 上用 Claude 桌面应用和 Codex 桌面应用实测(2026-10-23):两个方向都能在数秒内叫醒空闲的对话。已知限制:
104
105
 
105
106
  - Codex 只会为运行中的 Codex 应用里已加载的对话启动排队的一轮。Codex 重启后,用户点开对话才会加载。
106
107
  - Codex 的后台等待会随 Codex 应用退出而结束,要等该对话再结束一轮才会重新启动。这期间发来的消息留在 host 里为未读(不进入 Codex 自己的队列),直到用户下次在该对话里发消息。
@@ -4,6 +4,33 @@
4
4
 
5
5
  本项目的所有重大变更都将记录在此文件中。
6
6
 
7
+ ## [0.12.4] - 2026-10-30
8
+
9
+ ### 修复
10
+
11
+ - Windows 上空闲的 Codex 对话重新能被叫醒。Stop `--wait` hook 启动的后台等待进程继承了 Codex 读取 hook 输出用的管道(Codex 通过 PowerShell 运行 hook),Codex 一直等这些管道直到 hook 超时,然后连等待进程一起结束:会话从不显示为 `listening`,只在下次输入或调用工具时才看到消息。现在等待进程由 PowerShell 的 `Start-Process` 启动,它不传递任何句柄。
12
+
13
+ ### 变更
14
+
15
+ - 客户端回报失败的唤醒(`codex queue` 出错或退出码非 0)在 host 撤销它时记入 `host.log`:会话、唤醒 ID、再次送达的消息和通知各有几条。此前撤销和重试不留任何痕迹。
16
+
17
+ ## [0.12.3] - 2026-10-29
18
+
19
+ ### 修复
20
+
21
+ - Stop 事件丢失的会话(回合结束时 host 正好关闭或重启)像其他会话一样 30 分钟后过期:后台等待在会话处于 working 时不再算作它的活动,于是它不再被报告为 working,发给它的消息也不再等待一个永远不会到来的唤醒。
22
+ - 绑定到名字的 host(`--host mybox`)接受以该名字寻址的请求:它自己的健康探测、托盘和 `service install` 不再得到 403、不再报告没有应答。
23
+ - 资料文本(`set_profile` 的 title、note、label)、客户端报告的标题、主机名、WSL 发行版名和工作目录都按单行存储:其中的换行和 Unicode 行分隔符不能再在其他会话的通知里伪造消息的框架行。
24
+ - `leave_meeting` 之后重新加入会议的成员按文档所说从最新消息开始,而不是因错过的消息被拒绝第一次发言。
25
+ - 旧版本 host 写的数据库迁移失败时,提示在该版本的 CHANGELOG 要求重置数据目录时删除 `nexus.db`,而不是只给一条 SQL 错误。
26
+ - 客户端能承受端点上不是 n2n-nexus host 的情况:错误状态加非对象响应体不再让 hook 或 MCP 适配器崩溃;工具列表不是列表时适配器保持未连接(继续重试)而不是半连接;工具调用得到非对象响应体时作为传输错误失败。发现缓存先写临时文件再改名到位,并发的 hook 不会读到半截 URL。
27
+ - Windows 上通过 `install` 升级时,数据目录以反斜杠结尾且含空格也能带着原数据目录重启后台服务。
28
+
29
+ ### 变更
30
+
31
+ - **允许局域网访问**添加的 Windows 防火墙规则只对专用和域网络生效,不含公用网络。早先版本添加的规则在关闭再开启局域网访问之前不会改变。
32
+ - WSL 上 MCP 适配器的实例 ID 包含发行版名,因为同一台 Windows 上的发行版共用主机名;同一工作区的两个发行版不再共用聊天和会议的已读游标。WSL 用户的 ID 会变一次,这些游标随之重置(hooks 按对话记录的游标不受影响)。
33
+
7
34
  ## [0.12.2] - 2026-10-29
8
35
 
9
36
  ### 变更
package/docs/INSTALL.md CHANGED
@@ -114,7 +114,7 @@ The adapter can start before the host. It loads the host's tool list once the ho
114
114
  By default the host listens on `127.0.0.1` only. To make it the host for other machines on a trusted network:
115
115
 
116
116
  1. On the host machine, turn LAN access on:
117
- - Tray: check **Allow LAN access**. The tray writes `"host": "0.0.0.0"` to `settings.json` and restarts the host. On Windows it adds inbound firewall rules for the TCP port and the UDP discovery port (5688 and 5688 by default) after a UAC prompt; on macOS and Linux it tells you which ports to allow in your firewall.
117
+ - Tray: check **Allow LAN access**. The tray writes `"host": "0.0.0.0"` to `settings.json` and restarts the host. On Windows it adds inbound firewall rules for the TCP port and the UDP discovery port (5688 and 5688 by default) on private and domain networks, not public ones, after a UAC prompt; on macOS and Linux it tells you which ports to allow in your firewall.
118
118
  - Service: `n2n-nexus-host service install --host 0.0.0.0` writes the address to `settings.json`; allow TCP 5688 and UDP 5688 in the firewall yourself.
119
119
  2. On the other machines, install the client with `--endpoint discover` (for example `n2n-nexus install claude --endpoint discover`), or with the host's address.
120
120
 
@@ -114,7 +114,7 @@ Claude Code 不需要这一步:`install claude` 写入 `settings.json` 后,h
114
114
  host 默认只监听 `127.0.0.1`。要让它成为可信网络里其他机器的 host:
115
115
 
116
116
  1. 在 host 所在的机器上打开局域网访问:
117
- - 托盘:勾选 **允许局域网访问**。托盘把 `"host": "0.0.0.0"` 写进 `settings.json` 并重启 host。Windows 上会在 UAC 确认后添加 TCP 端口和 UDP 发现端口(默认都是 5688)的入站防火墙规则;macOS 和 Linux 上会提示需要在防火墙放行的端口。
117
+ - 托盘:勾选 **允许局域网访问**。托盘把 `"host": "0.0.0.0"` 写进 `settings.json` 并重启 host。Windows 上会在 UAC 确认后添加 TCP 端口和 UDP 发现端口(默认都是 5688)的入站防火墙规则,只对专用和域网络生效,不含公用网络;macOS 和 Linux 上会提示需要在防火墙放行的端口。
118
118
  - 服务:`n2n-nexus-host service install --host 0.0.0.0` 把地址写进 `settings.json`;防火墙里的 TCP 5688 和 UDP 5688 需要自己放行。
119
119
  2. 在其他机器上用 `--endpoint discover` 安装客户端(例如 `n2n-nexus install claude --endpoint discover`),或者直接写 host 的地址。
120
120
 
@@ -12,7 +12,7 @@ Parameter tables below are derived from `contract/tools.json`, the JSON Schema t
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
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`.
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).
18
18
  - All project write tools run synchronously and return their result directly. The task tools (`create_task`, `get_task`, `list_tasks`, `update_task`, `cancel_task`) track work that assistants manage themselves.
@@ -12,7 +12,7 @@
12
12
  - 调用按对话限流,没装 hooks 时按实例(默认每秒 20 次,突发 60 次;对应 host `settings.json` 中的 `rateLimit` 和 `rateBurst`)。超出限制的调用会失败并返回 "Rate limit exceeded ... retry after Ns";等待该时长后重试。
13
13
  - 每个工具都有供客户端界面显示的 `title`(例如 "Delete Project")。
14
14
  - 每个工具都带有 MCP annotations:`readOnlyHint`、`destructiveHint`、`idempotentHint` 和 `openWorldHint: false`(所有工具都只作用于 host)。按照 MCP 的定义,只有更新完全是追加时,工具才算非破坏性。读取类工具标为只读。`start_meeting`、`join_meeting`、`create_task`、`send_message`、`send` 和 `reply` 只追加数据,是非破坏性的(`join_meeting` 也是幂等的);`update_task` 也标为非破坏性且幂等。`listen`、`set_profile`、`sync_project_assets`、`update_project`、`upload_project_asset`、`rename_project`、`sync_global_doc`、`update_global_strategy`、`end_meeting`、`archive_meeting`、`reopen_meeting`、`leave_meeting`、`stance`、`cancel_task`、`host_maintenance` 和 `host_delete_project` 会改写或删除已有状态,标为破坏性,以便客户端请求确认。`read_messages` 和 `read` 不是只读的,因为它们会推进调用方自己的未读状态;但它们是非破坏性的,因为不改变任何共享数据。
15
- - 每个适配器都有一个实例 ID(`NEXUS_INSTANCE_ID`、`--id`,默认是 `mcp-<hostname>-<工作目录的哈希>-<IDE 名>`,其中 IDE 名是 MCP 客户端名,取自请求的 `io.modelcontextprotocol/clientInfo`,2025 代的客户端则取自 `initialize`)。同一个 IDE 在同一个工作区里重启后它保持不变,而同一个工作区里的两个 IDE 仍然互相区分。适配器在每次调用中都以 `instanceId` 带上它。同一个 IDE 在同一个工作区里的多个对话共用它,所以装了 n2n-nexus hooks 时,聊天和会议工具改为识别对话本身(见下一条);没装 hooks 的客户端按实例识别,这时没有 `instanceId` 的 `read_messages` 会失败。
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)中。
18
18
  - 所有项目写入工具都同步执行并直接返回结果。任务工具(`create_task`、`get_task`、`list_tasks`、`update_task`、`cancel_task`)用于跟踪由助手自己管理的工作。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n2n-nexus",
3
- "version": "0.12.2",
3
+ "version": "0.12.4",
4
4
  "description": "Local-first MCP coordination hub from N2NS Lab for multi-AI assistant collaboration.",
5
5
  "main": "build/index.js",
6
6
  "type": "module",