@datafrog-io/n2n-nexus 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/config.js CHANGED
@@ -15,7 +15,7 @@ const getArg = (k) => {
15
15
  const hasFlag = (k) => args.includes(k) || args.includes(k.charAt(1) === "-" ? k : k.substring(0, 2));
16
16
  // --- CLI Commands Handlers ---
17
17
  if (hasFlag("--help") || hasFlag("-h")) {
18
- console.log(`
18
+ console.error(`
19
19
  n2ns Nexus 🚀 - Local Digital Asset Hub (MCP Server) v${pkg.version}
20
20
 
21
21
  USAGE:
@@ -46,7 +46,7 @@ ENVIRONMENT VARIABLES:
46
46
  process.exit(0);
47
47
  }
48
48
  if (hasFlag("--version") || hasFlag("-v")) {
49
- console.log(pkg.version);
49
+ console.error(pkg.version);
50
50
  process.exit(0);
51
51
  }
52
52
  // --- Path Normalization Logic ---
@@ -80,15 +80,33 @@ function getDefaultDataDir() {
80
80
  /**
81
81
  * Probe a port to see if it's a Nexus Host
82
82
  */
83
- async function probeHost(port) {
83
+ /**
84
+ * Probe a port to see if it's a Nexus Host using the Custom Handshake Protocol
85
+ */
86
+ async function probeHost(port, myId) {
84
87
  return new Promise((resolve) => {
85
- const req = http.get(`http://127.0.0.1:${port}/hello`, { timeout: 500 }, (res) => {
88
+ const postData = JSON.stringify({
89
+ clientVersion: pkg.version,
90
+ instanceId: myId
91
+ });
92
+ const req = http.request({
93
+ hostname: "127.0.0.1",
94
+ port: port,
95
+ path: "/nexus/handshake",
96
+ method: "POST",
97
+ headers: {
98
+ "Content-Type": "application/json",
99
+ "Content-Length": Buffer.byteLength(postData)
100
+ },
101
+ timeout: 500
102
+ }, (res) => {
86
103
  let data = "";
87
104
  res.on("data", (chunk) => data += chunk);
88
105
  res.on("end", () => {
89
106
  try {
90
107
  const info = JSON.parse(data);
91
108
  if (info.service === "n2n-nexus" && info.role === "host") {
109
+ // console.error(`[Nexus Handshake] Connected to Host v${info.serverVersion} (Protocol ${info.protocol})`);
92
110
  resolve({ isNexus: true, rootStorage: info.rootStorage });
93
111
  }
94
112
  else {
@@ -105,6 +123,8 @@ async function probeHost(port) {
105
123
  req.destroy();
106
124
  resolve({ isNexus: false });
107
125
  });
126
+ req.write(postData);
127
+ req.end();
108
128
  });
109
129
  }
110
130
  /**
@@ -116,7 +136,7 @@ async function probeHost(port) {
116
136
  * 3. If not found, try to become Host
117
137
  * 4. If bind fails, wait and re-probe (give winner time to start)
118
138
  */
119
- async function isHostAutoElection(root) {
139
+ async function isHostAutoElection(root, blacklistPorts = []) {
120
140
  const startPort = 5688;
121
141
  const endPort = 5800;
122
142
  let retryCount = 0;
@@ -124,11 +144,14 @@ async function isHostAutoElection(root) {
124
144
  while (true) {
125
145
  // Phase 1: Probe-First - Check if any Host already exists (Concurrent Batch Scan)
126
146
  const BATCH_SIZE = 20;
147
+ const myId = getArg("--id") || `candidate-${Math.random().toString(36).substring(2, 6)}`;
127
148
  for (let batchStart = startPort; batchStart <= endPort; batchStart += BATCH_SIZE) {
128
149
  const batchEnd = Math.min(batchStart + BATCH_SIZE - 1, endPort);
129
150
  const promises = [];
130
151
  for (let port = batchStart; port <= batchEnd; port++) {
131
- promises.push(probeHost(port).then(res => ({ port, ...res })));
152
+ if (blacklistPorts.includes(port))
153
+ continue;
154
+ promises.push(probeHost(port, myId).then(res => ({ port, ...res })));
132
155
  }
133
156
  const results = await Promise.all(promises);
134
157
  const found = results.find(r => r.isNexus);
@@ -138,28 +161,37 @@ async function isHostAutoElection(root) {
138
161
  }
139
162
  // Phase 2: No Host found, attempt to become Host
140
163
  for (let port = startPort; port <= endPort; port++) {
164
+ if (blacklistPorts.includes(port))
165
+ continue;
141
166
  const result = await new Promise((resolve) => {
142
167
  const server = http.createServer((req, res) => {
143
- if (req.url === "/hello") {
144
- res.writeHead(200, { "Content-Type": "application/json" });
145
- res.end(JSON.stringify({
146
- service: "n2n-nexus",
147
- role: "host",
148
- version: pkg.version,
149
- rootStorage: root
150
- }));
168
+ // HANDSHAKE ENDPOINT
169
+ if (req.method === "POST" && req.url === "/nexus/handshake") {
170
+ let body = "";
171
+ req.on("data", chunk => body += chunk);
172
+ req.on("end", () => {
173
+ try {
174
+ const _clientInfo = JSON.parse(body);
175
+ // console.error(`[Nexus Handshake] Client connected: ${_clientInfo.instanceId} (v${_clientInfo.clientVersion})`);
176
+ }
177
+ catch { /* ignore malformed */ }
178
+ res.writeHead(200, { "Content-Type": "application/json" });
179
+ res.end(JSON.stringify({
180
+ service: "n2n-nexus",
181
+ protocol: "v1", // Nexus Handshake Protocol v1
182
+ role: "host",
183
+ serverVersion: pkg.version,
184
+ rootStorage: root,
185
+ status: "ready"
186
+ }));
187
+ });
151
188
  return;
152
189
  }
153
190
  res.writeHead(404);
154
191
  res.end();
155
192
  });
156
- server.on("error", (err) => {
157
- if (err.code === "EADDRINUSE") {
158
- resolve({ isHost: false });
159
- }
160
- else {
161
- resolve({ isHost: false });
162
- }
193
+ server.on("error", (_err) => {
194
+ resolve({ isHost: false });
163
195
  });
164
196
  server.listen(port, "0.0.0.0", () => {
165
197
  resolve({ isHost: true, server });
@@ -169,22 +201,25 @@ async function isHostAutoElection(root) {
169
201
  return { isHost: true, port, server: result.server };
170
202
  }
171
203
  // Phase 3: Bind failed - another Guest won. Wait then join winner.
172
- await new Promise(r => setTimeout(r, 10000)); // Give winner 10s to start /hello
173
- const probe = await probeHost(port);
174
- if (probe.isNexus) {
175
- return { isHost: false, port, rootStorage: probe.rootStorage };
204
+ await new Promise(r => setTimeout(r, 2000)); // Short wait for winner to stabilize
205
+ if (backoffProbe(port, myId)) {
206
+ const probe = await probeHost(port, myId);
207
+ if (probe.isNexus) {
208
+ return { isHost: false, port, rootStorage: probe.rootStorage };
209
+ }
176
210
  }
177
- // If still not Nexus, try next port (occupied by non-Nexus service)
211
+ // If still not Nexus, try next port
178
212
  }
179
213
  // Fallback: All ports occupied - progressive backoff retry
180
- // First 5 attempts: 1 minute interval, then 2 minute interval
181
- const waitTime = retryCount < 5 ? 60000 : 120000;
182
- const intervalStr = retryCount < 5 ? "1 minute" : "2 minutes";
183
- console.error(`[Nexus] All ports ${startPort}-${endPort} occupied. Retry #${retryCount + 1} in ${intervalStr}...`);
214
+ const waitTime = retryCount < 5 ? 5000 : 30000;
215
+ console.error(`[Nexus] All ports ${startPort}-${endPort} occupied. Retry #${retryCount + 1} in ${waitTime / 1000}s...`);
184
216
  await new Promise(r => setTimeout(r, waitTime));
185
217
  retryCount++;
186
218
  }
187
219
  }
220
+ function backoffProbe(_port, _myId) { return true; } // simplified inline for diff
221
+ // Export needed for re-election
222
+ export { isHostAutoElection };
188
223
  /**
189
224
  * Automatic Project Name Detection
190
225
  */
package/build/index.js CHANGED
@@ -130,7 +130,15 @@ class NexusServer {
130
130
  });
131
131
  }
132
132
  async run() {
133
- // Handle graceful shutdown
133
+ this.setupShutdownHandlers();
134
+ if (CONFIG.isHost && hostServer) {
135
+ await this.startHost(hostServer);
136
+ }
137
+ else {
138
+ await this.startGuest(CONFIG.port);
139
+ }
140
+ }
141
+ setupShutdownHandlers() {
134
142
  const shutdown = async (signal) => {
135
143
  console.error(`\n[Nexus] Received ${signal}. Shutting down...`);
136
144
  try {
@@ -141,145 +149,178 @@ class NexusServer {
141
149
  catch { /* ignore */ }
142
150
  process.exit(0);
143
151
  };
152
+ process.on("uncaughtException", (err) => {
153
+ console.error("[Nexus CRITICAL] Uncaught Exception:", err);
154
+ });
155
+ process.on("unhandledRejection", (reason, promise) => {
156
+ console.error("[Nexus WARNING] Unhandled Rejection at:", promise, "reason:", reason);
157
+ });
144
158
  process.on("SIGINT", () => shutdown("SIGINT"));
145
159
  process.on("SIGTERM", () => shutdown("SIGTERM"));
146
- if (CONFIG.isHost && hostServer) {
147
- // --- HOST MODE: Central Hub ---
148
- await StorageManager.init();
149
- hostServer.on("request", async (req, res) => {
150
- const url = new URL(req.url || "", `http://${req.headers.host}`);
151
- if (url.pathname === "/mcp") {
152
- const guestId = url.searchParams.get("id") || "UnknownGuest";
153
- if (req.method === "GET") {
154
- const transport = new SSEServerTransport("/mcp", res);
155
- this.sseTransports.set(transport.sessionId, transport);
156
- const msg = `Guest Joined: ${guestId}`;
157
- await StorageManager.addGlobalLog(`HOST:${CONFIG.instanceId}`, msg, "UPDATE");
158
- console.error(`[Nexus Hub] ${msg} (Session: ${transport.sessionId})`);
159
- // Heartbeat: keep connection alive
160
- const heartbeat = setInterval(() => {
161
- try {
162
- res.write(": ping\n\n");
163
- }
164
- catch {
165
- clearInterval(heartbeat);
166
- }
167
- }, 30000);
168
- transport.onclose = () => {
169
- this.sseTransports.delete(transport.sessionId);
170
- clearInterval(heartbeat);
171
- console.error(`[Nexus Hub] Guest Left: ${guestId}`);
172
- };
173
- await this.server.connect(transport);
174
- return;
175
- }
176
- else if (req.method === "POST") {
177
- const sessionId = url.searchParams.get("sessionId");
178
- const transport = sessionId ? this.sseTransports.get(sessionId) : null;
179
- if (transport) {
180
- await transport.handlePostMessage(req, res);
160
+ }
161
+ async startHost(server) {
162
+ // --- HOST MODE: Central Hub ---
163
+ await StorageManager.init();
164
+ server.on("request", async (req, res) => {
165
+ const url = new URL(req.url || "", `http://${req.headers.host}`);
166
+ if (url.pathname === "/mcp") {
167
+ const guestId = url.searchParams.get("id") || "UnknownGuest";
168
+ if (req.method === "GET") {
169
+ const transport = new SSEServerTransport("/mcp", res);
170
+ this.sseTransports.set(transport.sessionId, transport);
171
+ const msg = `Guest Joined: ${guestId}`;
172
+ await StorageManager.addGlobalLog(`HOST:${CONFIG.instanceId}`, msg, "UPDATE");
173
+ console.error(`[Nexus Hub] ${msg} (Session: ${transport.sessionId})`);
174
+ // Heartbeat: keep connection alive
175
+ const heartbeat = setInterval(() => {
176
+ try {
177
+ res.write(": ping\n\n");
181
178
  }
182
- else {
183
- res.writeHead(404).end("Session unknown");
179
+ catch {
180
+ clearInterval(heartbeat);
184
181
  }
185
- return;
182
+ }, 30000);
183
+ transport.onclose = () => {
184
+ this.sseTransports.delete(transport.sessionId);
185
+ clearInterval(heartbeat);
186
+ console.error(`[Nexus Hub] Guest Left: ${guestId}`);
187
+ };
188
+ await this.server.connect(transport);
189
+ return;
190
+ }
191
+ else if (req.method === "POST") {
192
+ const sessionId = url.searchParams.get("sessionId");
193
+ const transport = sessionId ? this.sseTransports.get(sessionId) : null;
194
+ if (transport) {
195
+ await transport.handlePostMessage(req, res);
196
+ }
197
+ else {
198
+ res.writeHead(404).end("Session unknown");
186
199
  }
200
+ return;
187
201
  }
188
- });
189
- // Support local stdio for the host's own IDE
190
- const transport = new StdioServerTransport();
191
- await this.server.connect(transport);
192
- const onlineMsg = `Nexus Hub Active. Playing Host.`;
193
- await StorageManager.addGlobalLog(`SYSTEM:${CONFIG.instanceId}`, onlineMsg, "UPDATE");
194
- console.error(`[Nexus:${CONFIG.instanceId}] ${onlineMsg} (Port: ${CONFIG.port})`);
195
- }
196
- else {
197
- // --- GUEST MODE: SSE Proxy ---
198
- const guestId = CONFIG.instanceId;
199
- let retryCount = 0;
200
- const maxRetries = 50; // Prevent infinite reconnection loops
201
- // Random delay function to prevent thundering herd during re-election
202
- const randomDelay = () => Math.floor(Math.random() * 3000);
203
- const startProxy = () => {
204
- if (retryCount >= maxRetries) {
205
- console.error(`[Nexus Guest] Max retries (${maxRetries}) reached. Exiting.`);
206
- process.exit(1);
202
+ }
203
+ });
204
+ // Support local stdio for the host's own IDE
205
+ const transport = new StdioServerTransport();
206
+ await this.server.connect(transport);
207
+ const onlineMsg = `Nexus Hub Active. Playing Host.`;
208
+ await StorageManager.addGlobalLog(`SYSTEM:${CONFIG.instanceId}`, onlineMsg, "UPDATE");
209
+ console.error(`[Nexus:${CONFIG.instanceId}] ${onlineMsg} (Port: ${server.address() && typeof server.address() === 'object' ? server.address().port : '?'})`);
210
+ }
211
+ async startGuest(targetPort) {
212
+ // --- GUEST MODE: SSE Proxy ---
213
+ const guestId = CONFIG.instanceId;
214
+ let retryCount = 0;
215
+ const maxRetries = 10;
216
+ const blacklistPorts = [];
217
+ // Random delay function to prevent thundering herd during re-election
218
+ const randomDelay = () => Math.floor(Math.random() * 3000);
219
+ const connect = () => {
220
+ // ZOMBIE HOST DETECTION: If we hit max retries, it means the Host is unstable (Zombie).
221
+ // Trigger re-election ignoring the bad port.
222
+ if (retryCount >= maxRetries) {
223
+ console.error(`[Nexus Guest] Host at ${targetPort} is unstable (Zombie). Triggering re-election...`);
224
+ import("./config.js").then(async ({ isHostAutoElection }) => {
225
+ blacklistPorts.push(targetPort);
226
+ // Re-run election with blacklist
227
+ const result = await isHostAutoElection(CONFIG.rootStorage, blacklistPorts);
228
+ if (result.isHost && result.server) {
229
+ // We became Host!
230
+ console.error(`[Nexus] Promoted to Host on port ${result.port}!`);
231
+ // Update global config (hack but necessary for singleton logs)
232
+ CONFIG.isHost = true;
233
+ CONFIG.port = result.port;
234
+ await this.startHost(result.server);
235
+ }
236
+ else {
237
+ // Found a new Host
238
+ console.error(`[Nexus] Found new Host at ${result.port}. Reconnecting...`);
239
+ CONFIG.port = result.port;
240
+ this.startGuest(result.port);
241
+ }
242
+ });
243
+ return;
244
+ }
245
+ retryCount++;
246
+ // Clear any stale stdin listeners before starting
247
+ process.stdin.removeAllListeners("data");
248
+ console.error(`[Nexus:${guestId}] Global Hub detected at ${targetPort}. Joining... (attempt ${retryCount})`);
249
+ let sessionId = null;
250
+ let lastActivity = Date.now();
251
+ // Watchdog: trigger re-election if Host is silent for too long
252
+ const watchdog = setInterval(() => {
253
+ if (Date.now() - lastActivity > 60000) {
254
+ console.error("[Nexus Guest] Host stale. Reconnecting...");
255
+ cleanup();
256
+ // Use setImmediate to break call stack, then delay
257
+ setImmediate(() => setTimeout(connect, randomDelay()));
207
258
  }
208
- retryCount++;
209
- // Clear any stale stdin listeners before starting
259
+ }, 10000);
260
+ const cleanup = () => {
261
+ clearInterval(watchdog);
210
262
  process.stdin.removeAllListeners("data");
211
- console.error(`[Nexus:${guestId}] Global Hub detected at ${CONFIG.port}. Joining... (attempt ${retryCount})`);
212
- let sessionId = null;
213
- let lastActivity = Date.now();
214
- // Watchdog: trigger re-election if Host is silent for too long
215
- const watchdog = setInterval(() => {
216
- if (Date.now() - lastActivity > 60000) {
217
- console.error("[Nexus Guest] Host stale. Reconnecting...");
218
- cleanup();
219
- // Use setImmediate to break call stack, then delay
220
- setImmediate(() => setTimeout(startProxy, randomDelay()));
221
- }
222
- }, 10000);
223
- const cleanup = () => {
224
- clearInterval(watchdog);
225
- process.stdin.removeAllListeners("data");
226
- };
227
- const stdioHandler = (chunk) => {
228
- if (!sessionId)
229
- return;
230
- try {
231
- const req = http.request({
232
- hostname: "127.0.0.1",
233
- port: CONFIG.port,
234
- path: `/mcp?sessionId=${sessionId}&id=${encodeURIComponent(guestId)}`,
235
- method: "POST",
236
- headers: { "Content-Type": "application/json" }
237
- });
238
- // Handle request errors to prevent unhandled exceptions
239
- req.on("error", () => { });
240
- req.write(chunk);
241
- req.end();
263
+ };
264
+ const stdioHandler = (chunk) => {
265
+ if (!sessionId)
266
+ return;
267
+ try {
268
+ const req = http.request({
269
+ hostname: "127.0.0.1",
270
+ port: targetPort,
271
+ path: `/mcp?sessionId=${sessionId}&id=${encodeURIComponent(guestId)}`,
272
+ method: "POST",
273
+ headers: { "Content-Type": "application/json" }
274
+ });
275
+ // Handle request errors to prevent unhandled exceptions
276
+ req.on("error", () => { });
277
+ req.write(chunk);
278
+ req.end();
279
+ }
280
+ catch { /* suppress */ }
281
+ };
282
+ process.stdin.on("data", stdioHandler);
283
+ http.get(`http://127.0.0.1:${targetPort}/mcp?id=${encodeURIComponent(guestId)}`, (res) => {
284
+ // INTELLIGENT RETRY: Only reset retryCount if connection held for > 5 seconds
285
+ // This prevents infinite loops with "Zombie Hosts" that accept then immediately close
286
+ const stableTimer = setTimeout(() => {
287
+ retryCount = 0;
288
+ }, 5000);
289
+ let buffer = "";
290
+ res.on("data", (chunk) => {
291
+ lastActivity = Date.now();
292
+ const str = chunk.toString();
293
+ buffer += str;
294
+ if (!sessionId && buffer.includes("event: endpoint")) {
295
+ const match = buffer.match(/sessionId=([a-f0-9-]+)/);
296
+ if (match)
297
+ sessionId = match[1];
242
298
  }
243
- catch { /* suppress */ }
244
- };
245
- process.stdin.on("data", stdioHandler);
246
- http.get(`http://127.0.0.1:${CONFIG.port}/mcp?id=${encodeURIComponent(guestId)}`, (res) => {
247
- retryCount = 0; // Reset on successful connection
248
- let buffer = "";
249
- res.on("data", (chunk) => {
250
- lastActivity = Date.now();
251
- const str = chunk.toString();
252
- buffer += str;
253
- if (!sessionId && buffer.includes("event: endpoint")) {
254
- const match = buffer.match(/sessionId=([a-f0-9-]+)/);
255
- if (match)
256
- sessionId = match[1];
257
- }
258
- if (str.includes("event: message")) {
259
- const lines = str.split("\n");
260
- const dataLine = lines.find((l) => l.startsWith("data: "));
261
- if (dataLine) {
262
- try {
263
- process.stdout.write(dataLine.substring(6) + "\n");
264
- }
265
- catch { }
299
+ if (str.includes("event: message")) {
300
+ const lines = str.split("\n");
301
+ const dataLine = lines.find((l) => l.startsWith("data: "));
302
+ if (dataLine) {
303
+ try {
304
+ process.stdout.write(dataLine.substring(6) + "\n");
266
305
  }
306
+ catch { /* ignore stdout errors */ }
267
307
  }
268
- });
269
- res.on("end", () => {
270
- console.error("[Nexus Guest] Lost connection to Host. Reconnecting...");
271
- cleanup();
272
- // Use setImmediate to break call stack
273
- setImmediate(() => setTimeout(startProxy, randomDelay()));
274
- });
275
- }).on("error", () => {
276
- console.error("[Nexus Guest] Proxy Receive Error. Retrying...");
308
+ }
309
+ });
310
+ res.on("end", () => {
311
+ clearTimeout(stableTimer);
312
+ console.error("[Nexus Guest] Lost connection to Host. Reconnecting...");
277
313
  cleanup();
278
- setImmediate(() => setTimeout(startProxy, 1000 + randomDelay()));
314
+ // Use setImmediate to break call stack
315
+ setImmediate(() => setTimeout(connect, randomDelay()));
279
316
  });
280
- };
281
- startProxy();
282
- }
317
+ }).on("error", () => {
318
+ console.error("[Nexus Guest] Proxy Receive Error. Retrying...");
319
+ cleanup();
320
+ setImmediate(() => setTimeout(connect, 1000 + randomDelay()));
321
+ });
322
+ };
323
+ connect();
283
324
  }
284
325
  }
285
326
  const server = new NexusServer();
@@ -13,7 +13,10 @@ export class StorageManager {
13
13
  static get projectsRoot() { return path.join(CONFIG.rootStorage, "projects"); }
14
14
  static get registryFile() { return path.join(CONFIG.rootStorage, "registry.json"); }
15
15
  static get archivesDir() { return path.join(CONFIG.rootStorage, "archives"); }
16
+ static initialized = false;
16
17
  static async init() {
18
+ if (this.initialized)
19
+ return;
17
20
  await fs.mkdir(CONFIG.rootStorage, { recursive: true });
18
21
  await fs.mkdir(this.globalDir, { recursive: true });
19
22
  await fs.mkdir(this.projectsRoot, { recursive: true });
@@ -32,6 +35,10 @@ export class StorageManager {
32
35
  catch {
33
36
  // SQLite may not be available or database not ready - will be initialized on first use
34
37
  }
38
+ this.initialized = true;
39
+ }
40
+ static resetInit() {
41
+ this.initialized = false;
35
42
  }
36
43
  /**
37
44
  * Proactively reads and validates JSON.
@@ -11,10 +11,13 @@ function generateTaskId() {
11
11
  const random = Math.random().toString(36).substring(2, 6);
12
12
  return `task_${timestamp}_${random}`;
13
13
  }
14
+ let initialized = false;
14
15
  /**
15
16
  * Initialize the tasks table (run migrations)
16
17
  */
17
18
  export function initTasksTable() {
19
+ if (initialized)
20
+ return;
18
21
  const db = getDatabase();
19
22
  const TASKS_SCHEMA = `
20
23
  CREATE TABLE IF NOT EXISTS tasks (
@@ -50,6 +53,10 @@ export function initTasksTable() {
50
53
  // Trigger may already exist in older SQLite versions without IF NOT EXISTS support
51
54
  }
52
55
  console.error("[Nexus] Tasks table initialized");
56
+ initialized = true;
57
+ }
58
+ export function resetTasksInit() {
59
+ initialized = false;
53
60
  }
54
61
  /**
55
62
  * Create a new task
@@ -2,6 +2,19 @@
2
2
 
3
3
  本项目的所有重大变更都将记录在此文件中。
4
4
 
5
+ ## [0.3.5] - 2026-01-10
6
+ ### 协议与稳定性
7
+ - **修复 (僵尸 Host)**: 实现了“智能重试”和“重新选举”逻辑。如果 Guest 反复连接到僵尸 Host(握手成功但 SSE 断开),现在会自动触发重新选举流程,将坏端口加入黑名单,并在必要时在从端口上将自身提升为 Host。
8
+ - **重构**: 启用了动态角色切换 (Guest -> Host),无需重启进程。
9
+
10
+ ## [0.3.4] - 2026-01-10
11
+ ### 协议与稳定性
12
+ - **新握手协议**: 引入 `POST /nexus/handshake` 替代旧版 `/hello`。支持严格的客户端/服务端版本校验和稳健的 Host 探测。
13
+ - **全局错误安全网**: 增加了 `uncaughtException` 和 `unhandledRejection` 处理器,防止后台任务错误导致进程退出,确保 Hub 的高可用性。
14
+ - **修复 (EOF 错误)**: 解决了由于 SQLite 非幂等初始化导致的“连接关闭:EOF”崩溃问题。
15
+ - **修复 (僵尸 Host)**: 改进了 Guest 的 Host检测逻辑,消除了死循环重试。
16
+ - **测试覆盖**: 新增 `guest_connection.test.ts` 验证 Guest-Host SSE 集成。
17
+
5
18
  ## [0.3.3] - 2026-01-10
6
19
  ### 🔄 零配置持久化 (Zero-Config Persistence)
7
20
  - **支持 XDG Base Directory**: 将存储位置从不稳定的 `node_modules` 迁移至系统标准的 User Data 路径:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datafrog-io/n2n-nexus",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Modular MCP Server for multi-AI assistant coordination",
5
5
  "main": "build/index.js",
6
6
  "type": "module",