n2n-nexus 0.12.3 → 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,16 @@
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
+
7
17
  ## [0.12.3] - 2026-10-29
8
18
 
9
19
  ### Fixed
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")) {
@@ -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,16 @@
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
+
7
17
  ## [0.12.3] - 2026-10-29
8
18
 
9
19
  ### 修复
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n2n-nexus",
3
- "version": "0.12.3",
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",