n2n-nexus 0.10.0 → 0.11.0
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 +15 -0
- package/build/cli/hook.js +3 -0
- package/build/cli/machine.js +27 -0
- package/build/server/nexus.js +8 -6
- package/contract/errors.json +2 -2
- package/contract/hook.json +36 -5
- package/contract/tools.json +105 -8
- package/docs/ARCHITECTURE.md +5 -5
- package/docs/ARCHITECTURE_zh.md +5 -5
- package/docs/ASSISTANT_GUIDE.md +3 -3
- package/docs/ASSISTANT_GUIDE_zh.md +3 -3
- package/docs/CHANGELOG_zh.md +15 -0
- package/docs/INSTALL.md +2 -0
- package/docs/INSTALL_zh.md +2 -0
- package/docs/TOOLS_REFERENCE.md +6 -6
- package/docs/TOOLS_REFERENCE_zh.md +6 -6
- package/docs/USER_GUIDE.md +1 -1
- package/docs/USER_GUIDE_zh.md +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
All notable changes to this project will be documented in this file.
|
|
6
6
|
|
|
7
|
+
## [0.11.0] - 2026-10-29
|
|
8
|
+
|
|
9
|
+
Upgrade the host and the client on every machine together, and delete `nexus.db` from the host's data directory before the new host starts: this release starts a new database and contract version, and an older host or client no longer works with it.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Sessions show the machine they run on. Every hook event carries `machine` (hostname without `.local`, platform `darwin` / `linux` / `wsl` / `win32`, local time minus UTC in minutes); the host refuses an event without it. `peers` and `set_profile` return `hostname`, `platform`, `utcOffset` and `label`; introductions and notifications show `name (<label or hostname> · <platform>)`; `read_messages` returns `senderMachine`. `set_profile({ label })` names the machine in place of its hostname.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Contract version 3: the host and the client must be upgraded together (`n2n-nexus doctor` reports a mismatch).
|
|
18
|
+
- The database schema is two files, one per owner, with no upgrade path from earlier databases: delete `nexus.db` before starting this host (it then says so if an old file is left).
|
|
19
|
+
- Notifications, the collaboration rules, the tool descriptions and the MCP instructions state facts only (sender, text, whether and how to reply) and no longer claim that a message is an approved instruction; that authorization lives only in the `CLAUDE.md` block that `install claude` writes. A Claude Code recipient gets the reply tool's full name.
|
|
20
|
+
- `send` / `read` / `reply` and `send_message` / `read_messages` name each other in their first sentence; the introduction, `peers` and the MCP instructions say that the client's own agent list does not show nexus sessions; `send` asks the sender to end its turn instead of polling with `read`.
|
|
21
|
+
|
|
7
22
|
## [0.10.0] - 2026-10-28
|
|
8
23
|
|
|
9
24
|
### Added
|
package/build/cli/hook.js
CHANGED
|
@@ -10,6 +10,7 @@ import { spawn } from "node:child_process";
|
|
|
10
10
|
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
11
|
import { NexusClient, NexusTransportError, REQUEST_TIMEOUT_CODE } from "../client/nexus-client.js";
|
|
12
12
|
import { DISCOVER_ENDPOINT, LOCAL_ENDPOINT, resolveEndpoint } from "../client/discovery.js";
|
|
13
|
+
import { machineInfo } from "./machine.js";
|
|
13
14
|
import { readTitle } from "./title.js";
|
|
14
15
|
/** Errors after which a discovered daemon is looked up again: it was never reached, so resending is safe. */
|
|
15
16
|
const UNREACHABLE_CODES = new Set(["ECONNREFUSED", "EHOSTUNREACH", "ENETUNREACH", "ETIMEDOUT", "EADDRNOTAVAIL"]);
|
|
@@ -48,6 +49,8 @@ async function forward(argv) {
|
|
|
48
49
|
if (title !== undefined)
|
|
49
50
|
input.title = title;
|
|
50
51
|
}
|
|
52
|
+
// Every event says where the conversation runs; the host refuses one without it.
|
|
53
|
+
input.machine = machineInfo();
|
|
51
54
|
// SessionEnd must finish within Codex's 3 s hook limit (discovery included): a short budget and a single attempt.
|
|
52
55
|
const sessionEnd = stdin.hook_event_name === "SessionEnd";
|
|
53
56
|
const timeoutMs = stdin.hook_event_name === "Stop" ? HOOK_STOP_REQUEST_TIMEOUT_MS : sessionEnd ? SESSION_END_REQUEST_TIMEOUT_MS : HOOK_REQUEST_TIMEOUT_MS;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where this conversation runs, which the hook command adds to the input of
|
|
3
|
+
* every event it forwards as `machine`, so that other sessions can tell the machines apart
|
|
4
|
+
* (several sessions share a name and a working directory across machines).
|
|
5
|
+
* Node built-ins only: the hook command imports this on client events.
|
|
6
|
+
*/
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
/** darwin, linux, win32 as Node reports them; wsl for a Linux that runs under Windows. */
|
|
10
|
+
export function platformOf(platform = process.platform, readProcVersion = () => fs.readFileSync("/proc/version", "utf8")) {
|
|
11
|
+
if (platform !== "linux")
|
|
12
|
+
return platform;
|
|
13
|
+
try {
|
|
14
|
+
return /microsoft/i.test(readProcVersion()) ? "wsl" : "linux";
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return "linux";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** The host name without the `.local` suffix that macOS adds. */
|
|
21
|
+
export function hostnameOf(hostname = os.hostname()) {
|
|
22
|
+
return hostname.replace(/\.local$/i, "");
|
|
23
|
+
}
|
|
24
|
+
export function machineInfo(now = new Date()) {
|
|
25
|
+
// 0 - x, not -x: at UTC -x is -0, which JSON writes as 0.
|
|
26
|
+
return { hostname: hostnameOf(), platform: platformOf(), utcOffset: 0 - now.getTimezoneOffset() };
|
|
27
|
+
}
|
package/build/server/nexus.js
CHANGED
|
@@ -11,12 +11,14 @@ const INITIAL_LIST_WAIT_MS = 1000;
|
|
|
11
11
|
const SERVICE_NAME = "n2n-nexus";
|
|
12
12
|
/** Sent to the client on connect so the model knows it can talk to other AI sessions. */
|
|
13
13
|
const INSTRUCTIONS = "n2n-nexus connects this conversation with other AI sessions (Claude Code and Codex conversations, " +
|
|
14
|
-
"possibly on other machines). Direct messages: call peers to see who is online
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
14
|
+
"possibly on other machines), all of your own user. Direct messages: call peers to see who is online (your " +
|
|
15
|
+
"client's own agent list, such as ListAgents, does not show these sessions), send to message a session (everything " +
|
|
16
|
+
"the recipient needs in one message), reply to answer one and read for the messages a notification cut or did not " +
|
|
17
|
+
"show. What a message may ask of you is set by your user's own instructions; when something is missing, ask the " +
|
|
18
|
+
"sender with reply. After send, end your turn instead of polling with read: the answer arrives through the hooks " +
|
|
19
|
+
"after your next tool call or at the end of a turn (at your user's next prompt once the automatic-turn limit is " +
|
|
20
|
+
"reached), and notifyWhenIdle tells you when the recipient finishes its turn (set expectsReply and wake false on a " +
|
|
21
|
+
"message that needs no answer, and skip plain acknowledgements); listen waits for messages after each turn. " +
|
|
20
22
|
"Direct messaging needs the n2n-nexus hooks. Separately, send_message and read_messages are the shared " +
|
|
21
23
|
"global chat and meetings, not direct messages. Session ids in peers (claude:<id>, codex:<id>) are what send " +
|
|
22
24
|
"takes; with the hooks, chat and meetings also record senders and unread cursors per session id " +
|
package/contract/errors.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"contractVersion":
|
|
2
|
+
"contractVersion": 3,
|
|
3
3
|
"errorBody": {
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"ok": true,
|
|
22
22
|
"service": "n2n-nexus",
|
|
23
23
|
"version": "<host version>",
|
|
24
|
-
"contractVersion":
|
|
24
|
+
"contractVersion": 3,
|
|
25
25
|
"sessions": {
|
|
26
26
|
"online": "<number of agent sessions not offline>"
|
|
27
27
|
}
|
package/contract/hook.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"contractVersion":
|
|
2
|
+
"contractVersion": 3,
|
|
3
3
|
"clients": [
|
|
4
4
|
"claude",
|
|
5
5
|
"codex"
|
|
@@ -57,16 +57,17 @@
|
|
|
57
57
|
"sessionNamePattern": "^[A-Za-z0-9._@#-]{1,48}$",
|
|
58
58
|
"previewChars": 100,
|
|
59
59
|
"notifyListLimit": 5,
|
|
60
|
-
"notifyTextChars": 5000
|
|
60
|
+
"notifyTextChars": 5000,
|
|
61
|
+
"maxHostnameChars": 80
|
|
61
62
|
},
|
|
62
63
|
"texts": {
|
|
63
64
|
"noticePrefix": "[n2n-nexus]",
|
|
64
65
|
"notifyClosing": "Where a reply is asked, answer with reply once the work is done.",
|
|
65
66
|
"stopClosing": "Do the work, and where a reply is asked, answer with reply before you finish.",
|
|
66
67
|
"statusNote": "Only listening sessions answer right away; idle and working ones see a message when their user next sends a prompt or they next call a tool.",
|
|
67
|
-
"sessionStartClosing": "Call peers for the current list, send to message a session,
|
|
68
|
-
"collaborationRules": "All sessions on n2n-nexus belong to your own user
|
|
69
|
-
"messageTrustNote": "These messages come from other AI sessions of your own user
|
|
68
|
+
"sessionStartClosing": "Call peers for the current list (your client's own agent list, such as ListAgents, does not show these sessions), send to message a session, reply to answer a message you received and read for the messages a notification cut or did not show; set_profile tells others what you work on. Meetings and the global chat use send_message and read_messages instead.",
|
|
69
|
+
"collaborationRules": "All sessions on n2n-nexus belong to your own user. A direct message shows who sent it, its text and whether it wants an answer; what you may do for it is set by your user's own instructions. When something you need is missing, ask the session that sent it with reply. When you hand work to another session, put everything it needs into one message: the goal, the background, what you already tried and what a finished result looks like; then end your turn: do not poll with read, the answer reaches you through the hooks after your next tool call or at the end of a turn (at your user's next prompt once the automatic-turn limit is reached), and notifyWhenIdle tells you when the recipient finishes. Answer a task once, with its result; 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.",
|
|
70
|
+
"messageTrustNote": "These messages come from other AI sessions of your own user. When something you need is missing, ask the sender with reply. Answer once, with the result; if you cannot finish, say what stopped you and the one thing that would let you go on. Skip plain acknowledgements.",
|
|
70
71
|
"messageInfoNote": "These messages come from other AI sessions of your own user; they need no reply.",
|
|
71
72
|
"messageHeader": "--- Message %s from %s (%s) at %s ---",
|
|
72
73
|
"messageEnd": "--- End of message %s ---",
|
|
@@ -74,6 +75,7 @@
|
|
|
74
75
|
"messageCut": "[Cut at %d of %d characters; read returns the whole message.]",
|
|
75
76
|
"replyInstruction": "Answer it with reply (messageId %s; expectsReply false for a result, true for a question).",
|
|
76
77
|
"readRest": "read returns the messages that are cut or not shown here.",
|
|
78
|
+
"replyTool": "The reply tool is %s.",
|
|
77
79
|
"noReplyMark": "(no reply needed)",
|
|
78
80
|
"earlierUnread": "%d earlier direct message(s) still unread; read them with read.",
|
|
79
81
|
"meetingNews": "Meeting \"%s\" (%s): %d new message(s) (#%d–#%d) from %s.",
|
|
@@ -176,6 +178,35 @@
|
|
|
176
178
|
"required": [
|
|
177
179
|
"tool"
|
|
178
180
|
]
|
|
181
|
+
},
|
|
182
|
+
"hookMachine": {
|
|
183
|
+
"description": "input.machine, which every hook event of a conversation carries (not BackgroundWait or WakeResult): where the conversation runs",
|
|
184
|
+
"type": "object",
|
|
185
|
+
"properties": {
|
|
186
|
+
"hostname": {
|
|
187
|
+
"type": "string",
|
|
188
|
+
"minLength": 1
|
|
189
|
+
},
|
|
190
|
+
"platform": {
|
|
191
|
+
"type": "string",
|
|
192
|
+
"enum": [
|
|
193
|
+
"darwin",
|
|
194
|
+
"linux",
|
|
195
|
+
"wsl",
|
|
196
|
+
"win32"
|
|
197
|
+
]
|
|
198
|
+
},
|
|
199
|
+
"utcOffset": {
|
|
200
|
+
"type": "integer",
|
|
201
|
+
"minimum": -1440,
|
|
202
|
+
"maximum": 1440
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
"required": [
|
|
206
|
+
"hostname",
|
|
207
|
+
"platform",
|
|
208
|
+
"utcOffset"
|
|
209
|
+
]
|
|
179
210
|
}
|
|
180
211
|
}
|
|
181
212
|
}
|
package/contract/tools.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"contractVersion":
|
|
2
|
+
"contractVersion": 3,
|
|
3
3
|
"tools": [
|
|
4
4
|
{
|
|
5
5
|
"name": "sync_project_assets",
|
|
@@ -925,7 +925,7 @@
|
|
|
925
925
|
{
|
|
926
926
|
"name": "peers",
|
|
927
927
|
"title": "List Peers",
|
|
928
|
-
"description": "List the AI sessions (Claude Code / Codex conversations) you can message, and your own profile;
|
|
928
|
+
"description": "List the AI sessions (Claude Code / Codex conversations) of your own user that you can message with send, and your own profile; your client's own agent list (such as ListAgents) does not show them. Each profile says which machine the conversation runs on: hostname and platform (darwin, linux, wsl or win32) and utcOffset (local time minus UTC, in minutes), as its hooks report them with every event, and the label it set with set_profile. Offline ones only with includeOffline. title is the conversation's title (from its client, or set with set_profile), for display only. status: listening (waiting for messages, answers right away), working or idle (sees a message when its user next sends a prompt or it next calls a tool), or offline (ended, or no activity for 30 minutes).",
|
|
929
929
|
"annotations": {
|
|
930
930
|
"readOnlyHint": true,
|
|
931
931
|
"openWorldHint": false
|
|
@@ -966,6 +966,30 @@
|
|
|
966
966
|
"cwd": {
|
|
967
967
|
"type": "string"
|
|
968
968
|
},
|
|
969
|
+
"hostname": {
|
|
970
|
+
"type": "string",
|
|
971
|
+
"description": "The machine the conversation runs on, as its hooks report it (without a .local suffix)"
|
|
972
|
+
},
|
|
973
|
+
"platform": {
|
|
974
|
+
"type": "string",
|
|
975
|
+
"enum": [
|
|
976
|
+
"darwin",
|
|
977
|
+
"linux",
|
|
978
|
+
"wsl",
|
|
979
|
+
"win32"
|
|
980
|
+
],
|
|
981
|
+
"description": "The operating system of that machine; wsl is a Linux distribution under Windows"
|
|
982
|
+
},
|
|
983
|
+
"utcOffset": {
|
|
984
|
+
"type": "integer",
|
|
985
|
+
"minimum": -1440,
|
|
986
|
+
"maximum": 1440,
|
|
987
|
+
"description": "The machine's local time minus UTC, in minutes"
|
|
988
|
+
},
|
|
989
|
+
"label": {
|
|
990
|
+
"type": "string",
|
|
991
|
+
"description": "A short machine label the session set with set_profile, shown in place of the hostname; \"\" when none"
|
|
992
|
+
},
|
|
969
993
|
"note": {
|
|
970
994
|
"type": "string"
|
|
971
995
|
},
|
|
@@ -988,6 +1012,10 @@
|
|
|
988
1012
|
"title",
|
|
989
1013
|
"client",
|
|
990
1014
|
"cwd",
|
|
1015
|
+
"hostname",
|
|
1016
|
+
"platform",
|
|
1017
|
+
"utcOffset",
|
|
1018
|
+
"label",
|
|
991
1019
|
"note",
|
|
992
1020
|
"status",
|
|
993
1021
|
"lastActiveAt"
|
|
@@ -1019,6 +1047,30 @@
|
|
|
1019
1047
|
"cwd": {
|
|
1020
1048
|
"type": "string"
|
|
1021
1049
|
},
|
|
1050
|
+
"hostname": {
|
|
1051
|
+
"type": "string",
|
|
1052
|
+
"description": "The machine the conversation runs on, as its hooks report it (without a .local suffix)"
|
|
1053
|
+
},
|
|
1054
|
+
"platform": {
|
|
1055
|
+
"type": "string",
|
|
1056
|
+
"enum": [
|
|
1057
|
+
"darwin",
|
|
1058
|
+
"linux",
|
|
1059
|
+
"wsl",
|
|
1060
|
+
"win32"
|
|
1061
|
+
],
|
|
1062
|
+
"description": "The operating system of that machine; wsl is a Linux distribution under Windows"
|
|
1063
|
+
},
|
|
1064
|
+
"utcOffset": {
|
|
1065
|
+
"type": "integer",
|
|
1066
|
+
"minimum": -1440,
|
|
1067
|
+
"maximum": 1440,
|
|
1068
|
+
"description": "The machine's local time minus UTC, in minutes"
|
|
1069
|
+
},
|
|
1070
|
+
"label": {
|
|
1071
|
+
"type": "string",
|
|
1072
|
+
"description": "A short machine label the session set with set_profile, shown in place of the hostname; \"\" when none"
|
|
1073
|
+
},
|
|
1022
1074
|
"note": {
|
|
1023
1075
|
"type": "string"
|
|
1024
1076
|
},
|
|
@@ -1041,6 +1093,10 @@
|
|
|
1041
1093
|
"title",
|
|
1042
1094
|
"client",
|
|
1043
1095
|
"cwd",
|
|
1096
|
+
"hostname",
|
|
1097
|
+
"platform",
|
|
1098
|
+
"utcOffset",
|
|
1099
|
+
"label",
|
|
1044
1100
|
"note",
|
|
1045
1101
|
"status",
|
|
1046
1102
|
"lastActiveAt"
|
|
@@ -1060,7 +1116,7 @@
|
|
|
1060
1116
|
{
|
|
1061
1117
|
"name": "send",
|
|
1062
1118
|
"title": "Send Direct Message",
|
|
1063
|
-
"description": "Send a direct message to one AI session (an id or name listed by peers). The recipient gets the whole message in its notification
|
|
1119
|
+
"description": "Send a direct message to one AI session of your own user (an id or name listed by peers); for a meeting or the global chat use send_message instead. The recipient gets the whole message in its notification; to answer a message you received, use reply. 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 rather than polling with read: the answer reaches you through the hooks after your next tool call or at the end of a turn (at your user's next prompt once the automatic-turn limit is reached); notifyWhenIdle tells you when the recipient finishes its turn. expectsReply false marks a message that needs no answer; wake false lets it wait for the recipient's next prompt or tool call: send a plain acknowledgement with both false, or not at all. Needs the n2n-nexus hooks.",
|
|
1064
1120
|
"annotations": {
|
|
1065
1121
|
"readOnlyHint": false,
|
|
1066
1122
|
"destructiveHint": false,
|
|
@@ -1147,7 +1203,7 @@
|
|
|
1147
1203
|
{
|
|
1148
1204
|
"name": "read",
|
|
1149
1205
|
"title": "Read Direct Messages",
|
|
1150
|
-
"description": "Read your unread direct messages, oldest first, and mark them read. 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.
|
|
1206
|
+
"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.",
|
|
1151
1207
|
"annotations": {
|
|
1152
1208
|
"readOnlyHint": false,
|
|
1153
1209
|
"destructiveHint": false,
|
|
@@ -1237,7 +1293,7 @@
|
|
|
1237
1293
|
{
|
|
1238
1294
|
"name": "reply",
|
|
1239
1295
|
"title": "Reply to Message",
|
|
1240
|
-
"description": "Reply to a direct message you received; 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.",
|
|
1296
|
+
"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.",
|
|
1241
1297
|
"annotations": {
|
|
1242
1298
|
"readOnlyHint": false,
|
|
1243
1299
|
"destructiveHint": false,
|
|
@@ -1353,7 +1409,7 @@
|
|
|
1353
1409
|
{
|
|
1354
1410
|
"name": "set_profile",
|
|
1355
1411
|
"title": "Set My Profile",
|
|
1356
|
-
"description": "Set your display name, a short note on what you are working on
|
|
1412
|
+
"description": "Set your display name, a short note on what you are working on, your conversation's title and/or a label for the machine you run on; others see them in peers, introductions and notifications. Pass at least one.",
|
|
1357
1413
|
"annotations": {
|
|
1358
1414
|
"readOnlyHint": false,
|
|
1359
1415
|
"destructiveHint": true,
|
|
@@ -1377,6 +1433,11 @@
|
|
|
1377
1433
|
"description": "Your conversation's title, shown in peers and notifications; replaces the title your client reports until you set it to \"\", which lets the client's title apply again",
|
|
1378
1434
|
"type": "string",
|
|
1379
1435
|
"maxLength": 80
|
|
1436
|
+
},
|
|
1437
|
+
"label": {
|
|
1438
|
+
"description": "A short label for the machine this conversation runs on (such as mac-air or win-desktop), shown in place of the hostname in peers, introductions and notifications; \"\" removes it",
|
|
1439
|
+
"type": "string",
|
|
1440
|
+
"maxLength": 32
|
|
1380
1441
|
}
|
|
1381
1442
|
}
|
|
1382
1443
|
},
|
|
@@ -1406,6 +1467,30 @@
|
|
|
1406
1467
|
"cwd": {
|
|
1407
1468
|
"type": "string"
|
|
1408
1469
|
},
|
|
1470
|
+
"hostname": {
|
|
1471
|
+
"type": "string",
|
|
1472
|
+
"description": "The machine the conversation runs on, as its hooks report it (without a .local suffix)"
|
|
1473
|
+
},
|
|
1474
|
+
"platform": {
|
|
1475
|
+
"type": "string",
|
|
1476
|
+
"enum": [
|
|
1477
|
+
"darwin",
|
|
1478
|
+
"linux",
|
|
1479
|
+
"wsl",
|
|
1480
|
+
"win32"
|
|
1481
|
+
],
|
|
1482
|
+
"description": "The operating system of that machine; wsl is a Linux distribution under Windows"
|
|
1483
|
+
},
|
|
1484
|
+
"utcOffset": {
|
|
1485
|
+
"type": "integer",
|
|
1486
|
+
"minimum": -1440,
|
|
1487
|
+
"maximum": 1440,
|
|
1488
|
+
"description": "The machine's local time minus UTC, in minutes"
|
|
1489
|
+
},
|
|
1490
|
+
"label": {
|
|
1491
|
+
"type": "string",
|
|
1492
|
+
"description": "A short machine label the session set with set_profile, shown in place of the hostname; \"\" when none"
|
|
1493
|
+
},
|
|
1409
1494
|
"note": {
|
|
1410
1495
|
"type": "string"
|
|
1411
1496
|
},
|
|
@@ -1428,6 +1513,10 @@
|
|
|
1428
1513
|
"title",
|
|
1429
1514
|
"client",
|
|
1430
1515
|
"cwd",
|
|
1516
|
+
"hostname",
|
|
1517
|
+
"platform",
|
|
1518
|
+
"utcOffset",
|
|
1519
|
+
"label",
|
|
1431
1520
|
"note",
|
|
1432
1521
|
"status",
|
|
1433
1522
|
"lastActiveAt"
|
|
@@ -1445,7 +1534,7 @@
|
|
|
1445
1534
|
{
|
|
1446
1535
|
"name": "send_message",
|
|
1447
1536
|
"title": "Post Message",
|
|
1448
|
-
"description": "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.
|
|
1537
|
+
"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.",
|
|
1449
1538
|
"annotations": {
|
|
1450
1539
|
"readOnlyHint": false,
|
|
1451
1540
|
"destructiveHint": false,
|
|
@@ -1537,7 +1626,7 @@
|
|
|
1537
1626
|
{
|
|
1538
1627
|
"name": "read_messages",
|
|
1539
1628
|
"title": "Read Chat / Meeting Messages",
|
|
1540
|
-
"description": "Read global chat (default) or one meeting's messages (meetingId). Unread mode (default) returns the messages of others this conversation has not fetched yet and moves its cursor past them; cursors are kept per conversation session when the n2n-nexus hooks are installed, else per MCP adapter instance (identity says which). A session's first unread read of global chat starts at the newest message. Message ids number a meeting's messages from 1; global chat has its own ids. History mode (latest or afterId) reads without a cursor and includes your own messages. Message 'from' is a session id (claude:<id> / codex:<id>, senderName its name) or an adapter instance id.
|
|
1629
|
+
"description": "Read a meeting or the global chat; your direct messages are read with read. Read global chat (default) or one meeting's messages (meetingId). Unread mode (default) returns the messages of others this conversation has not fetched yet and moves its cursor past them; cursors are kept per conversation session when the n2n-nexus hooks are installed, else per MCP adapter instance (identity says which). A session's first unread read of global chat starts at the newest message. Message ids number a meeting's messages from 1; global chat has its own ids. History mode (latest or afterId) reads without a cursor and includes your own messages. Message 'from' is a session id (claude:<id> / codex:<id>, senderName its name) or an adapter instance id.",
|
|
1541
1630
|
"annotations": {
|
|
1542
1631
|
"readOnlyHint": false,
|
|
1543
1632
|
"destructiveHint": false,
|
|
@@ -1617,6 +1706,13 @@
|
|
|
1617
1706
|
"null"
|
|
1618
1707
|
]
|
|
1619
1708
|
},
|
|
1709
|
+
"senderMachine": {
|
|
1710
|
+
"type": [
|
|
1711
|
+
"string",
|
|
1712
|
+
"null"
|
|
1713
|
+
],
|
|
1714
|
+
"description": "The sending session's machine, \"<label or hostname> · <platform>\"; null for an adapter instance"
|
|
1715
|
+
},
|
|
1620
1716
|
"text": {
|
|
1621
1717
|
"type": "string"
|
|
1622
1718
|
},
|
|
@@ -1645,6 +1741,7 @@
|
|
|
1645
1741
|
"id",
|
|
1646
1742
|
"from",
|
|
1647
1743
|
"senderName",
|
|
1744
|
+
"senderMachine",
|
|
1648
1745
|
"text",
|
|
1649
1746
|
"category",
|
|
1650
1747
|
"timestamp"
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -67,7 +67,7 @@ The adapter's instance ID (the identity of chat and meeting calls from clients w
|
|
|
67
67
|
|
|
68
68
|
### Conversations and hooks
|
|
69
69
|
|
|
70
|
-
Each Claude Code or Codex conversation is one session, `claude:<session id>` or `codex:<session id>`, using the ID the client itself assigns. `n2n-nexus install <claude|codex>` writes hooks that run `n2n-nexus hook <client>` on SessionStart, UserPromptSubmit, PostToolUse, Stop and SessionEnd (and, for Claude Code, PreToolUse on the n2n-nexus tools). The hook command forwards the event's identifying fields to `POST /api/hook` and prints the host's answer; on any error it exits 0 without output, so a missing host never blocks the client.
|
|
70
|
+
Each Claude Code or Codex conversation is one session, `claude:<session id>` or `codex:<session id>`, using the ID the client itself assigns. `n2n-nexus install <claude|codex>` writes hooks that run `n2n-nexus hook <client>` on SessionStart, UserPromptSubmit, PostToolUse, Stop and SessionEnd (and, for Claude Code, PreToolUse on the n2n-nexus tools). The hook command forwards the event's identifying fields to `POST /api/hook` and prints the host's answer; to every event it adds the conversation's machine (`hostname` without `.local`, `platform` with `wsl` told from `linux` by `/proc/version`, and `utcOffset`), which the host requires, stores with the session and shows after its name, because sessions on different machines often share a name and a working directory; on SessionStart, UserPromptSubmit and Stop it also adds the conversation's title; on any error it exits 0 without output, so a missing host never blocks the client.
|
|
71
71
|
|
|
72
72
|
```
|
|
73
73
|
Client event ──▶ n2n-nexus hook ──▶ POST /api/hook ──▶ session status, notifications
|
|
@@ -78,11 +78,11 @@ Stop ◀── decision: block ◀──┘ once pe
|
|
|
78
78
|
|
|
79
79
|
Tool calls name their conversation outside the model's arguments. The adapter reads it from the request's `_meta`: Claude Code sends `claudecode/toolUseId`, which the PreToolUse hook bound to the conversation just before the call (verified: the call waits for the hook); Codex sends `sessionId`. The adapter forwards it as `caller` in `POST /api/tools/call`. Because the identity is the conversation, not the adapter process, an adapter restart, a resumed conversation and a sub-agent all keep it, and one adapter can serve several Codex conversations.
|
|
80
80
|
|
|
81
|
-
Notifications carry each message whole (the texts of one notification together cut at `notifyTextChars`, 5000 characters; `read` returns a cut message), so the request reaches the model in the hook's injected context or Stop reason, where Claude Code weighs it as hook feedback, rather than only as tool output. They
|
|
81
|
+
Notifications carry each message whole (the texts of one notification together cut at `notifyTextChars`, 5000 characters; `read` returns a cut message), so the request reaches the model in the hook's injected context or Stop reason, where Claude Code weighs it as hook feedback, rather than only as tool output. They state 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); they claim no authorization, because the client's own notices present hook output as something other than user input, and a claim in the hook text would only contradict them. Claude Code acts on another session's message only where the user's own instructions call for it, so `install claude` adds a marked block with that instruction to the user-level `CLAUDE.md`, the one place that grants it. A message shown whole is marked read when the announcement drives the turn (Stop, listening, background wake; a failed wake makes it unread again, but a Stop text the client never read, because the user pressed Esc as it arrived, is gone with the message), and, if it needs no reply, when a prompt shows it. One that asks for a reply shown with a prompt, and every message shown after a tool call (that context can be a sub-agent's, which the conversation never sees), stays unread until `reply` answers it or `read` returns it, so Stop still holds the turn once for it. A listening Stop hook is held open for up to 590 s and answered as soon as a message arrives; the client killing the hook (the user pressing Esc) or the host shutting down ends the wait. This remains a trusted local host, not an authentication boundary: every process of the same operating-system user is trusted and could act as any conversation, and agents that must be isolated run as different users with their own hosts (see the local trust model in [SECURITY.md](../SECURITY.md)). See [tool reference](TOOLS_REFERENCE.md#direct-messages-between-conversations).
|
|
82
82
|
|
|
83
83
|
### Sessions, presence and waking idle conversations
|
|
84
84
|
|
|
85
|
-
A new conversation's SessionStart hook prints an introduction: its own name and ID, the other sessions that are not offline (up to five), how to use `peers`, `send` and `read`, and the collaboration rules (a message
|
|
85
|
+
A new conversation's SessionStart hook prints an introduction: its own name and ID, the other sessions that are not offline (up to five), how to use `peers`, `send` and `read`, that the client's own agent list does not show these sessions, and the collaboration rules (what a message may ask is set by the user's own instructions, with questions going to the sender; hand work on in one message with everything the recipient needs, then end the turn instead of polling with `read`; answer once with the result, or with what stopped the work and the one thing that would let it go on; no plain acknowledgements; no task passed back to its sender). The adapter also sends MCP server instructions with the same guidance. Clients do not always report the end of a conversation, so an `idle` or `working` session without activity for 30 minutes counts as `offline` until its next event.
|
|
86
86
|
|
|
87
87
|
An idle conversation is not running, so hooks alone can only reach it when its user acts. To let AI sessions carry out each other's requests and answer without anyone typing, `install` adds a second Stop hook, `n2n-nexus hook <client> --wait`, that starts a background wait when a turn ends:
|
|
88
88
|
|
|
@@ -114,13 +114,13 @@ The wait never delivers while the session is `working` or while a listening Stop
|
|
|
114
114
|
|
|
115
115
|
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)).
|
|
116
116
|
|
|
117
|
-
- **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
|
|
117
|
+
- **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.
|
|
118
118
|
- **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` and `is_default`), `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`, 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).
|
|
119
119
|
- **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.
|
|
120
120
|
- **Only the host opens the database**. MCP adapters and hooks go through HTTP.
|
|
121
121
|
- **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.
|
|
122
122
|
|
|
123
|
-
Meeting members and stances store their resolved identity kind; draft eligibility is captured in the publishing transaction.
|
|
123
|
+
Meeting members and stances store their resolved identity kind; draft eligibility is captured in the publishing transaction.
|
|
124
124
|
|
|
125
125
|
### Maintenance and retention
|
|
126
126
|
|
package/docs/ARCHITECTURE_zh.md
CHANGED
|
@@ -67,7 +67,7 @@ n2n-nexus(npm):无状态适配器,每条 MCP 连接一个
|
|
|
67
67
|
|
|
68
68
|
### 对话与 hooks
|
|
69
69
|
|
|
70
|
-
每个 Claude Code 或 Codex 对话是一个会话,ID 为 `claude:<会话 ID>` 或 `codex:<会话 ID>`,直接使用客户端自己分配的 ID。`n2n-nexus install <claude|codex>` 写入的 hooks 会在 SessionStart、UserPromptSubmit、PostToolUse、Stop、SessionEnd(Claude Code 还有只匹配 n2n-nexus 工具的 PreToolUse)时运行 `n2n-nexus hook <client>`。hook 命令把事件里用于识别的字段转发给 `POST /api/hook`,并打印 host
|
|
70
|
+
每个 Claude Code 或 Codex 对话是一个会话,ID 为 `claude:<会话 ID>` 或 `codex:<会话 ID>`,直接使用客户端自己分配的 ID。`n2n-nexus install <claude|codex>` 写入的 hooks 会在 SessionStart、UserPromptSubmit、PostToolUse、Stop、SessionEnd(Claude Code 还有只匹配 n2n-nexus 工具的 PreToolUse)时运行 `n2n-nexus hook <client>`。hook 命令把事件里用于识别的字段转发给 `POST /api/hook`,并打印 host 的回应;每个事件都附上对话所在的机器(去掉 `.local` 的 `hostname`、`platform`——用 `/proc/version` 区分 `wsl` 和 `linux`——以及 `utcOffset`),host 要求必须有,存进会话并显示在名字后面,因为不同机器上的会话常常名字和工作目录都一样;在 SessionStart、UserPromptSubmit 和 Stop 时还附上对话标题;出任何错误都以退出码 0 退出、不输出,所以 host 不在也不会卡住客户端。
|
|
71
71
|
|
|
72
72
|
```
|
|
73
73
|
客户端事件 ──▶ n2n-nexus hook ──▶ POST /api/hook ──▶ 会话状态、通知
|
|
@@ -78,11 +78,11 @@ Stop ◀── decision: block ◀─┘ 每次结
|
|
|
78
78
|
|
|
79
79
|
工具调用的对话身份不经过模型参数。适配器从请求的 `_meta` 里读取:Claude Code 带 `claudecode/toolUseId`,由调用前刚运行的 PreToolUse hook 绑定到对话(已实测:调用会等 hook 结束);Codex 带 `sessionId`。适配器把它作为 `caller` 放进 `POST /api/tools/call`。身份是对话而不是适配器进程,所以适配器重启、恢复对话、子代理都沿用同一身份,一个适配器也能同时服务多个 Codex 对话。
|
|
80
80
|
|
|
81
|
-
通知带上每条消息的完整正文(一条通知里的正文合计截到 `notifyTextChars`,即 5000 个字符;被截断的消息用 `read` 读全文),请求随 hook 注入的上下文或 Stop 的理由送到模型面前,Claude Code 把它当作 hook
|
|
81
|
+
通知带上每条消息的完整正文(一条通知里的正文合计截到 `notifyTextChars`,即 5000 个字符;被截断的消息用 `read` 读全文),请求随 hook 注入的上下文或 Stop 的理由送到模型面前,Claude Code 把它当作 hook 反馈来看待,而不只是工具结果。通知只陈述事实:发送方、正文、要不要回复、怎么回复(Claude Code 会话还给出工具全名);不声明任何授权,因为客户端自己的提示把 hook 输出说成"不是用户输入",hook 文本里的授权声明只会和它打架。Claude Code 只在用户自己的指令要求时才照其他会话的消息去做,所以 `install claude` 会在用户级 `CLAUDE.md` 里加一段写明这一点的标记内容,授权只来自那里。完整显示的消息在提醒驱动这一轮时(Stop、待命、后台唤醒;唤醒失败会恢复为未读,但消息到达的同时用户按下 Esc、客户端没读到的 Stop 文本会连同消息一起丢失)标为已读,无需回复的消息随提示词显示时也标为已读。随提示词显示、需要回复的消息,以及工具调用后显示的所有消息(那段上下文可能是子代理的,对话本身看不到),仍为未读,直到 `reply` 回复它或 `read` 读取它,本轮结束时 Stop 照样为它拦住一次。待命中的 Stop hook 最长挂起 590 秒,消息一到立即返回;客户端杀掉 hook(用户按 Esc)或 host 关闭时结束等待。系统仍采用可信本地 host 边界,这不是身份认证机制:同一系统用户下的所有进程都被视为可信,可以冒充任何对话;需要互相隔离的代理应以不同的系统用户运行,各自使用自己的 host(见[安全文档](SECURITY_zh.md#安全模型)中的本地信任模型)。精确契约见[工具参考](TOOLS_REFERENCE_zh.md#对话之间的直接消息)。
|
|
82
82
|
|
|
83
83
|
### 会话、在线状态与唤醒空闲对话
|
|
84
84
|
|
|
85
|
-
新对话的 SessionStart hook 会输出一段介绍:它自己的名称和 ID、没有离线的其他会话(最多列 5 个)、`peers`、`send`、`read`
|
|
85
|
+
新对话的 SessionStart hook 会输出一段介绍:它自己的名称和 ID、没有离线的其他会话(最多列 5 个)、`peers`、`send`、`read` 的用法,客户端自带的代理列表看不到这些会话,以及协作规则(消息可以要求什么由用户自己的指令决定,有疑问问发送方;交出工作时把对方需要的一切写进一条消息,发完就结束本轮而不是用 `read` 轮询;只回复一次结果,做不完时说明卡在哪里、需要什么才能继续;不发单纯的确认;不把任务推回给发来的会话)。适配器也会在 MCP 的服务器 instructions 里给出同样的说明。客户端不一定会报告对话结束,所以 `idle` 或 `working` 的会话 30 分钟没有任何活动就算 `offline`,直到它下一次有事件。
|
|
86
86
|
|
|
87
87
|
空闲的对话并没有在运行,只靠 hooks,要等它的用户有动作才能把消息送进去。为了让 AI 会话之间无需有人输入就能执行对方的请求并回复,`install` 会增加第二个 Stop hook `n2n-nexus hook <client> --wait`,每轮结束时启动一个后台等待:
|
|
88
88
|
|
|
@@ -114,13 +114,13 @@ Stop ◀── decision: block ◀─┘ 每次结
|
|
|
114
114
|
|
|
115
115
|
全部数据都在 host 数据目录里的一个 SQLite 文件 `nexus.db` 中,同一目录下还有 `settings.json` 和 `host.lock`(各系统的位置见[使用手册](USER_GUIDE_zh.md#数据和日志))。
|
|
116
116
|
|
|
117
|
-
- **表结构**:嵌入 host 的编号迁移脚本(`host/internal/store/migrations/0001_core.sql`、`0002_agents.sql`)在启动时按序执行,每个文件一个事务;版本号存在 `PRAGMA user_version
|
|
117
|
+
- **表结构**:嵌入 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);以后改表结构时新增编号文件。
|
|
118
118
|
- **表**:`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
119
|
- **事务**:每个写操作一个事务,多行修改(会议消息,开始 / 结束 / 重开会议,项目改名)要么全部生效,要么都不生效。数据库使用 WAL 模式,开启 `foreign_keys`,忙等超时 5 秒,只用一个连接。
|
|
120
120
|
- **只有 host 打开数据库**。MCP 适配器和 hooks 都走 HTTP。
|
|
121
121
|
- **活动日志**:通过工具产生的状态变更(项目同步 / 更新 / 改名 / 删除 / 资产、文档同步、策略更新、会议开始 / 结束 / 归档 / 重开、维护)记录为结构化事件,用 `list_events` 读取,不混入聊天流。
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
会议成员和表态记录实际解析出的身份类型;草案资格在发布事务中保存快照。
|
|
124
124
|
|
|
125
125
|
### 清理与保留期
|
|
126
126
|
|
package/docs/ASSISTANT_GUIDE.md
CHANGED
|
@@ -59,13 +59,13 @@ All write tools run synchronously and return their result directly.
|
|
|
59
59
|
|
|
60
60
|
In Claude Code and Codex with the n2n-nexus hooks installed, every conversation is a session you can message directly. `send`, `read` and `reply` are direct messages between conversations; global chat and meetings use `send_message` and `read_messages` (section 4). Sessions are identified by their name or session id, `claude:<id>` or `codex:<id>`.
|
|
61
61
|
|
|
62
|
-
1. `peers()` lists the other conversations (name, title, client, working directory, note, status) and your own profile. The title comes from your client; use `set_profile({ name, note, title })` so others can tell what you work on.
|
|
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. Add `notifyWhenIdle: true` to get one notice when that conversation finishes its turn, instead of polling `peers`.
|
|
62
|
+
1. `peers()` lists the other conversations (name, title, client, working directory, machine: hostname, platform, UTC offset and label, note, status) and your own profile; several sessions can share a name and a working directory on different machines, so tell them apart by the machine; your client's own agent list (such as Claude Code's `ListAgents`) does not show them. The title comes from your client; use `set_profile({ name, note, title, label })` so others can tell what you work on and which machine you are (`label`, such as `mac-air`, replaces the hostname in what others see).
|
|
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
66
|
5. To wait for messages after your turn, call `listen({ on: true })`; turn it off with `listen({ on: false })`.
|
|
67
67
|
|
|
68
|
-
Messages come from other AI sessions of your own user
|
|
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
69
|
|
|
70
70
|
### 4. Messages and collaboration
|
|
71
71
|
|
|
@@ -59,13 +59,13 @@ upload_project_asset(projectId, fileName, base64Content) → 上传二进制文
|
|
|
59
59
|
|
|
60
60
|
在装好 n2n-nexus hooks 的 Claude Code 和 Codex 里,每个对话都是一个可以直接发消息的会话。`send`、`read`、`reply` 是对话之间的定向消息;全局聊天和会议用 `send_message`、`read_messages`(第 4 节)。会话用名字或会话 ID(`claude:<id>`、`codex:<id>`)标识。
|
|
61
61
|
|
|
62
|
-
1. `peers()`
|
|
63
|
-
2. `send({ to: 名字或 ID, text })`
|
|
62
|
+
1. `peers()` 列出其他对话(名字、标题、客户端、工作目录、机器:主机名、平台、UTC 偏移和标签、备注、状态)和你自己的资料;不同机器上的会话可能名字和工作目录都一样,要靠机器区分;客户端自带的代理列表(如 Claude Code 的 `ListAgents`)看不到它们。标题来自你的客户端;用 `set_profile({ name, note, title, label })` 让别人知道你在做什么、在哪台机器上(`label` 如 `mac-air`,别人看到时取代主机名)。
|
|
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
66
|
5. 想在本轮结束后继续等消息,调用 `listen({ on: true })`;用 `listen({ on: false })` 关闭。
|
|
67
67
|
|
|
68
|
-
消息来自同一个用户的其他 AI
|
|
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
69
|
|
|
70
70
|
### 4. 消息与协作
|
|
71
71
|
|
package/docs/CHANGELOG_zh.md
CHANGED
|
@@ -4,6 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
本项目的所有重大变更都将记录在此文件中。
|
|
6
6
|
|
|
7
|
+
## [0.11.0] - 2026-10-29
|
|
8
|
+
|
|
9
|
+
每台机器上的 host 和客户端要一起升级,并在新 host 启动前删除 host 数据目录里的 `nexus.db`:这个版本启用新的数据库和契约版本,旧的 host 或客户端不能再和它配合使用。
|
|
10
|
+
|
|
11
|
+
### 新增
|
|
12
|
+
|
|
13
|
+
- 会话显示所在的机器。每个 hook 事件都带 `machine`(去掉 `.local` 的主机名、平台 `darwin` / `linux` / `wsl` / `win32`、本地时间减 UTC 的分钟数),缺少时 host 拒收。`peers` 和 `set_profile` 返回 `hostname`、`platform`、`utcOffset` 和 `label`;介绍和通知显示 `名字 (<标签或主机名> · <平台>)`;`read_messages` 返回 `senderMachine`。`set_profile({ label })` 给机器起名,显示时取代主机名。
|
|
14
|
+
|
|
15
|
+
### 变更
|
|
16
|
+
|
|
17
|
+
- 契约版本 3:host 和客户端必须一起升级(`n2n-nexus doctor` 会报告不匹配)。
|
|
18
|
+
- 数据库表结构改为每个模块一个文件,不再支持从旧数据库升级:启动这个 host 前删除 `nexus.db`(留着旧文件时 host 会提示)。
|
|
19
|
+
- 通知、协作规则、工具描述和 MCP 说明只陈述事实(发送方、正文、要不要回复、怎么回复),不再声明消息是已批准的指令;授权只保留在 `install claude` 写进 `CLAUDE.md` 的那段里。Claude Code 收件方会看到回复工具的全名。
|
|
20
|
+
- `send` / `read` / `reply` 和 `send_message` / `read_messages` 的描述第一句互相指引;介绍、`peers` 和 MCP 说明写明客户端自带的代理列表看不到 nexus 会话;`send` 提示发送方结束本轮,不要用 `read` 轮询。
|
|
21
|
+
|
|
7
22
|
## [0.10.0] - 2026-10-28
|
|
8
23
|
|
|
9
24
|
### 新增
|
package/docs/INSTALL.md
CHANGED
|
@@ -138,6 +138,8 @@ IPv6 discovery keeps the HTTP address family: a host bound to `::` on this machi
|
|
|
138
138
|
|
|
139
139
|
## 8. Upgrading
|
|
140
140
|
|
|
141
|
+
Upgrade the host and the client on every machine to the same release: a host and a client of different contract versions refuse each other's requests, and `n2n-nexus doctor` reports the mismatch. Read the CHANGELOG entry of the release first; it says when the host's data directory has to be reset.
|
|
142
|
+
|
|
141
143
|
- Client: the MCP server needs nothing: each new conversation starts the latest version (a running conversation keeps the one it started with). For the hooks, run `npm i -g n2n-nexus@latest`; they use the new version at once.
|
|
142
144
|
- Host: upgrade with the package manager you installed it with (`scoop update n2n-nexus-host`, `brew upgrade n2n-nexus-host`). A Windows install from the release zip upgrades with `n2n-nexus-host.exe install` from the newer release, which restarts the tray and the background service itself. Otherwise replace the program, then restart it (tray: **Restart**; service: run `n2n-nexus-host service install` again from an administrator terminal).
|
|
143
145
|
|
package/docs/INSTALL_zh.md
CHANGED
|
@@ -138,6 +138,8 @@ IPv6 发现会保持 HTTP 的地址族:本机监听 `::` 时使用 `[::1]`,
|
|
|
138
138
|
|
|
139
139
|
## 8. 升级
|
|
140
140
|
|
|
141
|
+
每台机器上的 host 和客户端都要升到同一个版本:契约版本不同的 host 和客户端会拒绝对方的请求,`n2n-nexus doctor` 会报告不匹配。升级前先看该版本的 CHANGELOG,需要重置 host 数据目录时那里会写明。
|
|
142
|
+
|
|
141
143
|
- 客户端:MCP 服务器不用管,每次新开对话都会启动最新版本(正在进行的对话保持它启动时的版本)。hooks 需要运行 `npm i -g n2n-nexus@latest`,之后立即用上新版本。
|
|
142
144
|
- host:用当初安装它的包管理器升级(`scoop update n2n-nexus-host`、`brew upgrade n2n-nexus-host`)。Windows 上用发布的 zip 安装的,在新版本的发布包里运行 `n2n-nexus-host.exe install` 即可,它会自己重启托盘和后台服务。其他情况直接替换程序后重启(托盘:**重启**;服务:在管理员终端里再运行一次 `n2n-nexus-host service install`)。
|
|
143
145
|
|
package/docs/TOOLS_REFERENCE.md
CHANGED
|
@@ -154,15 +154,15 @@ These six tools are direct messages between conversations. Global chat and meeti
|
|
|
154
154
|
| `read` | `count?: integer = 10` (1–50) | `{ messages: [{ messageId, from: { id, name }, replyTo, text, createdAt, expectsReply }], remaining }`; unread messages oldest first, marked read. |
|
|
155
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. |
|
|
156
156
|
| `listen` | `on: boolean` | `{ listening }`. While on, each time the conversation ends a turn its Stop hook waits up to 590 seconds and continues the conversation when a message or notice arrives. In Claude Code, pressing Esc ends one wait. |
|
|
157
|
-
| `set_profile` | `name?: string` (1–48 of `A–Z a–z 0–9 . _ @ # -`), `note?: string` (≤ 200 chars), `title?: string` (≤ 80 chars); at least one | `{ profile }`. Names are unique among sessions that are not offline. A `title` you set replaces the one from your client until you set it to `""`. |
|
|
157
|
+
| `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
158
|
|
|
159
|
-
`Profile` contains `id`, `name`, `title`, `client` (`claude` or `codex`), `cwd`, `note`, `status` and `lastActiveAt`. `title` is the conversation's title as its client shows it (Claude Code: the conversation's title or the name you gave it; Codex: the thread's name, else its first prompt), read by the hooks when a conversation starts, at each prompt and when a turn ends, up to 80 characters; `""` until there is one. It is shown next to the name in `peers`, introductions, notifications and meeting members; `name` stays the handle for `send`. A session is first named `<client>@<working directory name>`, with `#2`, `#3`, … when that name is taken by a session that is not offline. `status` is `working` (the user sent a prompt or a tool ran), `idle` (the turn ended; a session with a background wait for messages shows as `listening` instead), `listening` (waiting in listen mode) or `offline` (the conversation ended). Clients do not always report the end of a conversation, so an `idle` or `working` session without any activity for 30 minutes becomes `offline` too; its next event brings it back. Only a `listening` session answers right away; `idle` and `working` ones see a message when their user next sends a prompt or they next call a tool.
|
|
159
|
+
`Profile` contains `id`, `name`, `title`, `client` (`claude` or `codex`), `cwd`, `hostname`, `platform`, `utcOffset`, `label`, `note`, `status` and `lastActiveAt`. `hostname` (without a `.local` suffix), `platform` (`darwin`, `linux`, `wsl` for a Linux distribution under Windows, or `win32`) 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>` after the name, for example `claude@_projects#2 (MacBook-Air · macOS)`. `title` is the conversation's title as its client shows it (Claude Code: the conversation's title or the name you gave it; Codex: the thread's name, else its first prompt), read by the hooks when a conversation starts, at each prompt and when a turn ends, up to 80 characters; `""` until there is one. It is shown next to the name in `peers`, introductions, notifications and meeting members; `name` stays the handle for `send`. A session is first named `<client>@<working directory name>`, with `#2`, `#3`, … when that name is taken by a session that is not offline. `status` is `working` (the user sent a prompt or a tool ran), `idle` (the turn ended; a session with a background wait for messages shows as `listening` instead), `listening` (waiting in listen mode) or `offline` (the conversation ended). Clients do not always report the end of a conversation, so an `idle` or `working` session without any activity for 30 minutes becomes `offline` too; its next event brings it back. Only a `listening` session answers right away; `idle` and `working` ones see a message when their user next sends a prompt or they next call a tool.
|
|
160
160
|
|
|
161
|
-
Delivery. The recipient's hooks announce new messages 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
|
|
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, one line per meeting (see [`join_meeting`](#join_meeting)):
|
|
162
162
|
|
|
163
163
|
| Client event | What the recipient sees |
|
|
164
164
|
| --- | --- |
|
|
165
|
-
| Conversation started | Its own name and ID, the other sessions that are not offline (up to five listed), which of them can answer right away, how to use `peers`, `send` and `read`, and the collaboration rules: a message
|
|
165
|
+
| Conversation started | Its own name and ID, the other sessions that are not offline (up to five listed), which of them can answer right away, how to use `peers`, `send` and `read`, that the client's own agent list does not show these sessions, and the collaboration rules: what a message may ask is set by the user's own instructions, with questions about it going to the sender; work handed on goes in one message with everything the recipient needs, and the sender then ends its turn instead of polling with `read`; a task is answered once with its result, or with what stopped it and the one thing that would let it go on; no plain acknowledgements, and no task passed back to its sender. |
|
|
166
166
|
| Prompt submitted | New messages (up to five shown), the count of unread ones announced before, pending notices and meeting news. The prompt that starts a woken turn adds only what arrived after the wake. |
|
|
167
167
|
| After each tool call | Only messages, notices and meeting news not announced yet. |
|
|
168
168
|
| Turn about to end | If an unread message may wake the session (`wake` true), a notice is pending or a meeting the session takes part in actively has news, the turn continues once with the announcement. |
|
|
@@ -172,7 +172,7 @@ Delivery. The recipient's hooks announce new messages with their whole text: eac
|
|
|
172
172
|
|
|
173
173
|
Unread messages expire after 24 hours; the sender then gets a notice. Notices also report `notifyWhenIdle` results.
|
|
174
174
|
|
|
175
|
-
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
|
|
175
|
+
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, 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
176
|
|
|
177
177
|
### `send_message`
|
|
178
178
|
|
|
@@ -210,7 +210,7 @@ Behavior:
|
|
|
210
210
|
- Unread mode is incremental per conversation (per adapter instance without hooks): each call returns messages of others not yet returned to it, oldest first, and moves its cursor past them and past its own messages.
|
|
211
211
|
- A conversation's first unread read of global chat starts at the newest message: it returns nothing and a `note`; read earlier messages with `latest` or `afterId`. Its first read of a meeting starts at the meeting's first message.
|
|
212
212
|
- History mode moves no cursor, includes your own messages and needs no identity.
|
|
213
|
-
- Result: `{ source: "global" | "meeting", mode: "unread" | "history", identity?, meetingId?, messages: [{ id, from, senderName, text, category, timestamp }], count, note? }`. `from` is the sender's session ID (`senderName` is its name, usable with `send`) or instance ID (`senderName` null); `category` is null for a message without one. A meeting message's `id` is its number in the meeting, from 1; global chat messages have their own ids.
|
|
213
|
+
- Result: `{ source: "global" | "meeting", mode: "unread" | "history", identity?, meetingId?, messages: [{ id, from, senderName, senderMachine, text, category, timestamp }], count, note? }`. `from` is the sender's session ID (`senderName` is its name, usable with `send`, and `senderMachine` its machine as `<label or hostname> · <platform>`) or instance ID (`senderName` and `senderMachine` null); `category` is null for a message without one. A meeting message's `id` is its number in the meeting, from 1; global chat messages have their own ids.
|
|
214
214
|
- Unread mode without hooks needs the calling instance: a call without `instanceId` fails with `read_messages needs the instanceId field of the POST /api/tools/call body (the n2n-nexus MCP adapter sends it automatically); unread cursors are kept per instance.` The MCP adapter always sends one.
|
|
215
215
|
|
|
216
216
|
### `list_events`
|
|
@@ -154,15 +154,15 @@
|
|
|
154
154
|
| `read` | `count?: integer = 10`(1–50) | `{ messages: [{ messageId, from: { id, name }, replyTo, text, createdAt, expectsReply }], remaining }`;未读消息,最早的在前,读取后标为已读。 |
|
|
155
155
|
| `reply` | `messageId: string`、`text: string`(1–32000 个字符)、`expectsReply?: boolean = true`、`wake?: boolean = true` | 结果与 `send` 相同。该消息必须是发给你的(它的 ID 在通知和 `read` 里);回复发给它的发送方,`replyTo` 设为该消息,被回复的消息标为已读。 |
|
|
156
156
|
| `listen` | `on: boolean` | `{ listening }`。开启期间,对话每结束一轮,其 Stop hook 会等待最多 590 秒,有消息或通知到达时让对话继续。在 Claude Code 中按 Esc 会结束一次等待。 |
|
|
157
|
-
| `set_profile` | `name?: string`(1–48 个 `A–Z a–z 0–9 . _ @ # -` 中的字符)、`note?: string`(≤ 200 个字符)、`title?: string`(≤ 80 个字符);至少提供一个 | `{ profile }`。名称在未离线的会话中唯一。 自己设置的 `title` 会取代客户端提供的标题,直到设为 `""
|
|
157
|
+
| `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
158
|
|
|
159
|
-
`Profile` 包含 `id`、`name`、`title`、`client`(`claude` 或 `codex`)、`cwd`、`note`、`status` 和 `lastActiveAt`。`title` 是客户端里显示的对话标题(Claude Code:对话标题或你给它起的名字;Codex:线程名,没有则为第一句提问),由 hooks 在对话开始、每次提交提示词和本轮结束时读取,最多 80 个字符;还没有时为 `""`。它显示在 `peers`、介绍、通知和会议成员里名称的旁边;`send` 仍用 `name`。会话的初始名称为 `<client>@<工作目录名>`,该名称已被某个未离线会话占用时加上 `#2`、`#3`、…。`status` 为 `working`(用户发送了提示词或有工具在运行)、`idle`(本轮已结束;有后台等待消息的会话改为显示 `listening`)、`listening`(在 listen 模式下等待)或 `offline`(对话已结束)。客户端不一定会报告对话结束,所以 `idle` 或 `working` 的会话 30 分钟没有任何活动也会变为 `offline`;它的下一个事件会让它恢复。只有 `listening` 的会话会立即回应;`idle` 和 `working` 的会话要等它的用户下次发送提示词或它下次调用工具时才会看到消息。
|
|
159
|
+
`Profile` 包含 `id`、`name`、`title`、`client`(`claude` 或 `codex`)、`cwd`、`hostname`、`platform`、`utcOffset`、`label`、`note`、`status` 和 `lastActiveAt`。`hostname`(去掉 `.local` 后缀)、`platform`(`darwin`、`linux`、`wsl` 表示 Windows 下的 Linux 发行版、`win32`)和 `utcOffset`(本地时间减 UTC 的分钟数)由会话的 hooks 随每个事件上报。介绍、通知和会议消息在名字后面显示机器,格式为 `<label 或主机名> · <macOS | Linux | WSL | Windows>`,例如 `claude@_projects#2 (MacBook-Air · macOS)`。`title` 是客户端里显示的对话标题(Claude Code:对话标题或你给它起的名字;Codex:线程名,没有则为第一句提问),由 hooks 在对话开始、每次提交提示词和本轮结束时读取,最多 80 个字符;还没有时为 `""`。它显示在 `peers`、介绍、通知和会议成员里名称的旁边;`send` 仍用 `name`。会话的初始名称为 `<client>@<工作目录名>`,该名称已被某个未离线会话占用时加上 `#2`、`#3`、…。`status` 为 `working`(用户发送了提示词或有工具在运行)、`idle`(本轮已结束;有后台等待消息的会话改为显示 `listening`)、`listening`(在 listen 模式下等待)或 `offline`(对话已结束)。客户端不一定会报告对话结束,所以 `idle` 或 `working` 的会话 30 分钟没有任何活动也会变为 `offline`;它的下一个事件会让它恢复。只有 `listening` 的会话会立即回应;`idle` 和 `working` 的会话要等它的用户下次发送提示词或它下次调用工具时才会看到消息。
|
|
160
160
|
|
|
161
|
-
送达。收件方的 hooks 提醒新消息时带上完整正文:每条列出的消息夹在 `--- Message <id> from <发送方> (<会话 ID>) at <时间> ---` 和 `--- End of message <id> ---` 之间,前面是它回复的消息,后面是怎么回复它。一条通知里所有正文合计截到 5000 个字符(`notifyTextChars`);被截断的消息会标出来,`read`
|
|
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
162
|
|
|
163
163
|
| 客户端事件 | 收件方看到的内容 |
|
|
164
164
|
| --- | --- |
|
|
165
|
-
| 对话开始 | 自己的名称和 ID、未离线的其他会话(最多列出五个)、其中哪些能立即回应、`peers`、`send` 和 `read`
|
|
165
|
+
| 对话开始 | 自己的名称和 ID、未离线的其他会话(最多列出五个)、其中哪些能立即回应、`peers`、`send` 和 `read` 的用法,客户端自带的代理列表看不到这些会话,以及协作规则:消息可以要求什么由用户自己的指令决定,有疑问问发送方;把工作交给别的会话时,对方需要的一切都写进一条消息,发完就结束本轮而不是用 `read` 轮询;任务只回复一次结果,做不完时说明卡在哪里、需要什么才能继续;不发单纯的确认,也不把任务推回给发来的会话。 |
|
|
166
166
|
| 提交提示词 | 新消息(最多显示五条)、之前提醒过的未读消息数、待处理的通知和会议新消息。被叫醒的那一轮开始时的提示词只附带叫醒之后新到的内容。 |
|
|
167
167
|
| 每次工具调用后 | 只有尚未提醒过的消息、通知和会议新消息。 |
|
|
168
168
|
| 本轮即将结束 | 有可以叫醒会话的未读消息(`wake` 为 true)、待处理的通知,或会话以 active 方式参加的会议有新消息时,本轮带着提醒再继续一次。 |
|
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
|
|
173
173
|
未读消息 24 小时后过期,发送方随后会收到通知。通知也会报告 `notifyWhenIdle` 的结果。
|
|
174
174
|
|
|
175
|
-
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
|
|
175
|
+
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, 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
176
|
|
|
177
177
|
### `send_message`
|
|
178
178
|
|
|
@@ -210,7 +210,7 @@ HTTP 客户端:hooks 调用 `POST /api/hook`,请求体为 `{ client: "claude
|
|
|
210
210
|
- 未读模式按对话增量进行(没装 hooks 时按适配器实例):每次调用返回尚未返回给它的他人消息,最早的在前,游标越过这些消息和它自己发的消息。
|
|
211
211
|
- 对话第一次以未读模式读全局聊天时从最新消息开始:本次不返回消息,带一条 `note`;更早的消息用 `latest` 或 `afterId` 读。第一次读某个会议时从该会议的第一条开始。
|
|
212
212
|
- 历史模式不动游标,包含自己的消息,也不需要识别身份。
|
|
213
|
-
- 结果:`{ source: "global" | "meeting", mode: "unread" | "history", identity?, meetingId?, messages: [{ id, from, senderName, text, category, timestamp }], count, note? }`。`from` 是发送方的会话 ID(`senderName` 是它的名字,可用于 `send
|
|
213
|
+
- 结果:`{ source: "global" | "meeting", mode: "unread" | "history", identity?, meetingId?, messages: [{ id, from, senderName, senderMachine, text, category, timestamp }], count, note? }`。`from` 是发送方的会话 ID(`senderName` 是它的名字,可用于 `send`,`senderMachine` 是它的机器,格式 `<label 或主机名> · <平台>`)或实例 ID(`senderName` 和 `senderMachine` 为 null);没有类别的消息 `category` 为 null。会议消息的 `id` 是它在该会议里的编号,从 1 开始;全局聊天的消息有自己的 ID。
|
|
214
214
|
- 没装 hooks 时未读模式需要调用方实例:没有 `instanceId` 的调用会失败,并返回 `read_messages needs the instanceId field of the POST /api/tools/call body (the n2n-nexus MCP adapter sends it automatically); unread cursors are kept per instance.` MCP 适配器总会带上它。
|
|
215
215
|
|
|
216
216
|
### `list_events`
|
package/docs/USER_GUIDE.md
CHANGED
|
@@ -24,7 +24,7 @@ Use the Code tab for sessions that should talk to each other.
|
|
|
24
24
|
|
|
25
25
|
## 3. Direct messages between conversations
|
|
26
26
|
|
|
27
|
-
Each conversation appears in `peers`. A new direct message is announced to the recipient automatically: when the user sends a prompt, after each tool call, and once before the turn ends. The notification shows the message's text (`read` returns a message it cut), and the recipient
|
|
27
|
+
Each conversation appears in `peers`. A new direct message is announced to the recipient automatically: when the user sends a prompt, after each tool call, and once before the turn ends. The notification shows the message's text (`read` returns a message it cut), and what the recipient may do for it is set by your own instructions: for Claude Code, the block that `n2n-nexus install claude` writes into your user-level `CLAUDE.md` says to carry it out as your instruction without asking you for confirmation and without going beyond it; a question about it goes back to the sending session, so you do not have to switch terminals. An idle conversation is woken too: when a turn ends, a background hook waits up to an hour for anything new and then starts a turn with it (Claude Code through an `asyncRewake` hook, Codex through `codex queue`), so AI sessions carry out each other's requests and answer without anyone typing. An unattended `claude -p` run is not kept waiting: the hook returns at once there. A Codex conversation is woken only while it is loaded in the running Codex app, and not again after Codex restarts until it ends a turn; see [Architecture](ARCHITECTURE.md#sessions-presence-and-waking-idle-conversations) for the limits. With `listen` on, a conversation that finished its turn keeps waiting for up to about 10 minutes and continues when a message arrives; while it waits, input you type is queued until you press Esc.
|
|
28
28
|
|
|
29
29
|
## 4. Check that it works
|
|
30
30
|
|
package/docs/USER_GUIDE_zh.md
CHANGED
|
@@ -24,7 +24,7 @@ Claude 桌面版里有两种对话:
|
|
|
24
24
|
|
|
25
25
|
## 3. 对话之间的直接消息
|
|
26
26
|
|
|
27
|
-
每个对话都会出现在 `peers` 里。新消息会自动通知收件方:用户发来新消息时、每次工具调用后、本轮结束前一次。通知里直接显示正文(被截断的消息用 `read`
|
|
27
|
+
每个对话都会出现在 `peers` 里。新消息会自动通知收件方:用户发来新消息时、每次工具调用后、本轮结束前一次。通知里直接显示正文(被截断的消息用 `read` 读取全文),收件方可以为它做什么由你自己的指令决定:对 Claude Code,`n2n-nexus install claude` 写进用户级 `CLAUDE.md` 的那段说明让它当作你的指令照要求执行、不找你确认、不超出范围;有疑问会问发来的那个会话,你不用切换终端。空闲的对话也会被叫醒:每轮结束时,一个后台 hook 最多等待一小时,有新消息就让对话开始新的一轮(Claude Code 通过 `asyncRewake` hook,Codex 通过 `codex queue`),AI 会话之间无需有人输入就能执行对方的请求并回复。无人值守的 `claude -p` 不会被它拖住:这时该 hook 立即返回。Codex 对话只有在运行中的 Codex 应用里已加载时才能被叫醒;Codex 重启后,要等该对话再结束一轮才会恢复,限制详见[架构说明](ARCHITECTURE_zh.md#会话在线状态与唤醒空闲对话)。开启 `listen` 后,对话结束一轮时会继续等待最多约 10 分钟,消息一到就接着处理;等待期间你输入的内容会排队,按 Esc 才会立即发出。
|
|
28
28
|
|
|
29
29
|
## 4. 检查是否正常
|
|
30
30
|
|