eacn3 0.5.0 → 0.6.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/AGENT_GUIDE.md +345 -0
- package/dist/index.js +75 -62
- package/dist/index.js.map +1 -1
- package/dist/server.js +588 -229
- package/dist/server.js.map +1 -1
- package/dist/src/event-transport.d.ts +24 -7
- package/dist/src/event-transport.js +92 -114
- package/dist/src/event-transport.js.map +1 -1
- package/dist/src/models.d.ts +12 -8
- package/dist/src/models.js.map +1 -1
- package/dist/src/network-client.d.ts +2 -0
- package/dist/src/network-client.js +83 -9
- package/dist/src/network-client.js.map +1 -1
- package/dist/src/reverse-control.js +3 -3
- package/dist/src/reverse-control.js.map +1 -1
- package/dist/src/state.d.ts +37 -4
- package/dist/src/state.js +406 -58
- package/dist/src/state.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -2
- package/scripts/cli.cjs +29 -12
package/dist/server.js
CHANGED
|
@@ -11,9 +11,27 @@ import { z } from "zod";
|
|
|
11
11
|
import { EACN3_DEFAULT_NETWORK_ENDPOINT } from "./src/models.js";
|
|
12
12
|
import * as state from "./src/state.js";
|
|
13
13
|
import * as net from "./src/network-client.js";
|
|
14
|
-
import * as
|
|
14
|
+
import * as transport from "./src/event-transport.js";
|
|
15
15
|
import * as a2a from "./src/a2a-server.js";
|
|
16
16
|
import * as rc from "./src/reverse-control.js";
|
|
17
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
18
|
+
import { join, dirname } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Activity log — file-based traceability
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
const __log_dir = join(dirname(fileURLToPath(import.meta.url)), "..", "logs");
|
|
24
|
+
let __activity_log = null;
|
|
25
|
+
function _writeActivityLog(line) {
|
|
26
|
+
try {
|
|
27
|
+
if (!__activity_log) {
|
|
28
|
+
mkdirSync(__log_dir, { recursive: true });
|
|
29
|
+
__activity_log = join(__log_dir, "activity.log");
|
|
30
|
+
}
|
|
31
|
+
appendFileSync(__activity_log, line + "\n");
|
|
32
|
+
}
|
|
33
|
+
catch { }
|
|
34
|
+
}
|
|
17
35
|
// ---------------------------------------------------------------------------
|
|
18
36
|
// Helper: MCP text result
|
|
19
37
|
// ---------------------------------------------------------------------------
|
|
@@ -33,15 +51,19 @@ function ok(data) {
|
|
|
33
51
|
function err(message) {
|
|
34
52
|
return { content: [{ type: "text", text: JSON.stringify({ error: message }) }] };
|
|
35
53
|
}
|
|
36
|
-
/** Log MCP tool calls to stderr for traceability. */
|
|
54
|
+
/** Log MCP tool calls to stderr AND activity.log for traceability. */
|
|
37
55
|
function logToolCall(toolName, params) {
|
|
38
56
|
const ts = new Date().toISOString();
|
|
39
|
-
|
|
57
|
+
const line = `[MCP] ${ts} CALL ${toolName} params=${JSON.stringify(params)}`;
|
|
58
|
+
console.error(line);
|
|
59
|
+
_writeActivityLog(line);
|
|
40
60
|
}
|
|
41
61
|
function logToolResult(toolName, success, detail) {
|
|
42
62
|
const ts = new Date().toISOString();
|
|
43
63
|
const tag = success ? "OK" : "ERR";
|
|
44
|
-
|
|
64
|
+
const line = `[MCP] ${ts} ${tag} ${toolName}${detail ? ` ${detail}` : ""}`;
|
|
65
|
+
console.error(line);
|
|
66
|
+
_writeActivityLog(line);
|
|
45
67
|
}
|
|
46
68
|
/**
|
|
47
69
|
* Resolve agent ID: use provided value, or auto-inject from state.
|
|
@@ -58,18 +80,117 @@ function resolveAgentId(provided) {
|
|
|
58
80
|
throw new Error("No agents registered. Call eacn3_register_agent first.");
|
|
59
81
|
throw new Error(`Multiple agents registered (${agents.map(a => a.agent_id).join(", ")}). Specify agent_id explicitly.`);
|
|
60
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Detect whether an API error indicates the server/agent registration is stale
|
|
85
|
+
* on the backend (e.g. "server not found", "agent not found", 404 on discovery).
|
|
86
|
+
*/
|
|
87
|
+
function isStaleRegistrationError(e) {
|
|
88
|
+
const msg = (e instanceof Error ? e.message : String(e)).toLowerCase();
|
|
89
|
+
return (msg.includes("404") ||
|
|
90
|
+
msg.includes("not found") ||
|
|
91
|
+
msg.includes("not registered") ||
|
|
92
|
+
msg.includes("unknown server") ||
|
|
93
|
+
msg.includes("unknown agent"));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Wrap a tool's async handler to auto-reconnect on stale registration errors.
|
|
97
|
+
* If the first attempt fails because the backend forgot our server/agent,
|
|
98
|
+
* trigger autoReconnect() and retry once.
|
|
99
|
+
*/
|
|
100
|
+
async function withAutoReconnect(fn) {
|
|
101
|
+
try {
|
|
102
|
+
return await fn();
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
if (isStaleRegistrationError(e) && !reconnecting) {
|
|
106
|
+
console.error(`[EACN3] stale registration detected, auto-reconnecting before retry...`);
|
|
107
|
+
await autoReconnect();
|
|
108
|
+
// Retry once after reconnect
|
|
109
|
+
return await fn();
|
|
110
|
+
}
|
|
111
|
+
throw e;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
61
114
|
// ---------------------------------------------------------------------------
|
|
62
|
-
// Heartbeat background interval
|
|
115
|
+
// Heartbeat background interval with auto-reconnect
|
|
63
116
|
// ---------------------------------------------------------------------------
|
|
64
117
|
let heartbeatInterval = null;
|
|
118
|
+
let heartbeatConsecutiveFailures = 0;
|
|
119
|
+
let reconnecting = false;
|
|
120
|
+
const HEARTBEAT_FAIL_THRESHOLD = 2; // trigger reconnect after 2 consecutive heartbeat failures
|
|
121
|
+
/**
|
|
122
|
+
* Attempt to re-register server and agents with the network.
|
|
123
|
+
* Called when heartbeat failures indicate the backend has forgotten us.
|
|
124
|
+
*/
|
|
125
|
+
async function autoReconnect() {
|
|
126
|
+
if (reconnecting)
|
|
127
|
+
return;
|
|
128
|
+
reconnecting = true;
|
|
129
|
+
console.error("[EACN3] auto-reconnect: heartbeat failures exceeded threshold, re-registering...");
|
|
130
|
+
const s = state.getState();
|
|
131
|
+
try {
|
|
132
|
+
// Try heartbeat once more — maybe transient
|
|
133
|
+
try {
|
|
134
|
+
await net.heartbeat();
|
|
135
|
+
heartbeatConsecutiveFailures = 0;
|
|
136
|
+
console.error("[EACN3] auto-reconnect: heartbeat recovered, no re-registration needed");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
catch { /* still failing */ }
|
|
140
|
+
// Re-register server
|
|
141
|
+
const res = await net.registerServer("0.5.1", "plugin://local", "plugin-user");
|
|
142
|
+
const newSid = res.server_id;
|
|
143
|
+
s.server_card = {
|
|
144
|
+
server_id: newSid,
|
|
145
|
+
version: "0.5.1",
|
|
146
|
+
endpoint: "plugin://local",
|
|
147
|
+
owner: "plugin-user",
|
|
148
|
+
status: "online",
|
|
149
|
+
};
|
|
150
|
+
state.saveServerData();
|
|
151
|
+
// Re-register all owned agents with the new server_id
|
|
152
|
+
for (const agent of state.listAgents()) {
|
|
153
|
+
agent.server_id = newSid;
|
|
154
|
+
try {
|
|
155
|
+
await net.registerAgent(agent);
|
|
156
|
+
console.error(`[EACN3] auto-reconnect: re-registered agent ${agent.agent_id}`);
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
console.error(`[EACN3] auto-reconnect: failed to re-register agent ${agent.agent_id}: ${e.message}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
state.save();
|
|
163
|
+
heartbeatConsecutiveFailures = 0;
|
|
164
|
+
console.error("[EACN3] auto-reconnect: completed successfully");
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
console.error(`[EACN3] auto-reconnect: failed: ${e.message}`);
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
reconnecting = false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
65
173
|
function startHeartbeat() {
|
|
66
174
|
if (heartbeatInterval)
|
|
67
175
|
return;
|
|
176
|
+
heartbeatConsecutiveFailures = 0;
|
|
177
|
+
// Wire up the connection-degraded callback from network-client
|
|
178
|
+
net.setConnectionDegradedCallback(() => {
|
|
179
|
+
console.error("[EACN3] connection degraded (multiple request failures), scheduling reconnect");
|
|
180
|
+
autoReconnect();
|
|
181
|
+
});
|
|
68
182
|
heartbeatInterval = setInterval(async () => {
|
|
69
183
|
try {
|
|
70
184
|
await net.heartbeat();
|
|
185
|
+
heartbeatConsecutiveFailures = 0;
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
heartbeatConsecutiveFailures++;
|
|
189
|
+
console.error(`[EACN3] heartbeat failed (${heartbeatConsecutiveFailures}/${HEARTBEAT_FAIL_THRESHOLD}): ${e.message}`);
|
|
190
|
+
if (heartbeatConsecutiveFailures >= HEARTBEAT_FAIL_THRESHOLD) {
|
|
191
|
+
autoReconnect();
|
|
192
|
+
}
|
|
71
193
|
}
|
|
72
|
-
catch { /* silent */ }
|
|
73
194
|
}, 60_000);
|
|
74
195
|
}
|
|
75
196
|
function stopHeartbeat() {
|
|
@@ -77,11 +198,14 @@ function stopHeartbeat() {
|
|
|
77
198
|
clearInterval(heartbeatInterval);
|
|
78
199
|
heartbeatInterval = null;
|
|
79
200
|
}
|
|
201
|
+
heartbeatConsecutiveFailures = 0;
|
|
80
202
|
}
|
|
81
203
|
// ---------------------------------------------------------------------------
|
|
82
204
|
// MCP Server
|
|
83
205
|
// ---------------------------------------------------------------------------
|
|
84
|
-
const server = new McpServer({ name: "eacn3", version: "0.5.
|
|
206
|
+
const server = new McpServer({ name: "eacn3", version: "0.5.1" }, {
|
|
207
|
+
capabilities: { logging: {} },
|
|
208
|
+
});
|
|
85
209
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
86
210
|
// Health / Cluster (2)
|
|
87
211
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -89,12 +213,15 @@ const server = new McpServer({ name: "eacn3", version: "0.5.0" });
|
|
|
89
213
|
server.tool("eacn3_health", "Check if a network node is alive and responding. No prerequisites — works before eacn3_connect. Returns {status: 'ok'} on success. Use this to verify an endpoint before connecting.", {
|
|
90
214
|
endpoint: z.string().optional().describe("Node URL to probe. Defaults to configured network endpoint."),
|
|
91
215
|
}, async (params) => {
|
|
216
|
+
logToolCall("eacn3_health", params);
|
|
92
217
|
const target = params.endpoint ?? state.getState().network_endpoint;
|
|
93
218
|
try {
|
|
94
219
|
const health = await net.checkHealth(target);
|
|
220
|
+
logToolResult("eacn3_health", true);
|
|
95
221
|
return ok({ endpoint: target, ...health });
|
|
96
222
|
}
|
|
97
223
|
catch (e) {
|
|
224
|
+
logToolResult("eacn3_health", false, e.message);
|
|
98
225
|
return err(`Health check failed for ${target}: ${e.message}`);
|
|
99
226
|
}
|
|
100
227
|
});
|
|
@@ -115,7 +242,7 @@ server.tool("eacn3_cluster_status", "Retrieve the full cluster topology includin
|
|
|
115
242
|
// Server Management (4)
|
|
116
243
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
117
244
|
// #1 eacn3_connect
|
|
118
|
-
server.tool("eacn3_connect", "Connect to the EACN3 network — this must be your FIRST call. Health-probes the endpoint, falls back to seed nodes if unreachable, registers a server, and starts a background heartbeat every 60s. Returns {server_id, network_endpoint, fallback,
|
|
245
|
+
server.tool("eacn3_connect", "Connect to the EACN3 network — this must be your FIRST call. Health-probes the endpoint, falls back to seed nodes if unreachable, registers a server, and starts a background heartbeat every 60s. Returns {server_id, network_endpoint, fallback, available_agents, hint}. IMPORTANT: agents are NOT auto-restored. Check available_agents — if you have a previous agent, call eacn3_claim_agent(agent_id) to resume it. Otherwise call eacn3_register_agent() to create a new one. Only one agent per session.", {
|
|
119
246
|
network_endpoint: z.string().optional().describe(`Network URL. Defaults to ${EACN3_DEFAULT_NETWORK_ENDPOINT}`),
|
|
120
247
|
seed_nodes: z.array(z.string()).optional().describe("Additional seed node URLs for fallback"),
|
|
121
248
|
}, async (params) => {
|
|
@@ -143,11 +270,11 @@ server.tool("eacn3_connect", "Connect to the EACN3 network — this must be your
|
|
|
143
270
|
}
|
|
144
271
|
catch {
|
|
145
272
|
// Server no longer known to network — re-register
|
|
146
|
-
const res = await net.registerServer("0.5.
|
|
273
|
+
const res = await net.registerServer("0.5.1", "plugin://local", "plugin-user");
|
|
147
274
|
sid = res.server_id;
|
|
148
275
|
s.server_card = {
|
|
149
276
|
server_id: sid,
|
|
150
|
-
version: "0.5.
|
|
277
|
+
version: "0.5.1",
|
|
151
278
|
endpoint: "plugin://local",
|
|
152
279
|
owner: "plugin-user",
|
|
153
280
|
status: "online",
|
|
@@ -163,48 +290,30 @@ server.tool("eacn3_connect", "Connect to the EACN3 network — this must be your
|
|
|
163
290
|
}
|
|
164
291
|
}
|
|
165
292
|
else {
|
|
166
|
-
const res = await net.registerServer("0.5.
|
|
293
|
+
const res = await net.registerServer("0.5.1", "plugin://local", "plugin-user");
|
|
167
294
|
sid = res.server_id;
|
|
168
295
|
s.server_card = {
|
|
169
296
|
server_id: sid,
|
|
170
|
-
version: "0.5.
|
|
297
|
+
version: "0.5.1",
|
|
171
298
|
endpoint: "plugin://local",
|
|
172
299
|
owner: "plugin-user",
|
|
173
300
|
status: "online",
|
|
174
301
|
};
|
|
175
302
|
}
|
|
176
|
-
state.
|
|
303
|
+
state.saveServerData();
|
|
177
304
|
// Start background heartbeat
|
|
178
305
|
startHeartbeat();
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
await net.getAgentInfo(agent.agent_id);
|
|
183
|
-
}
|
|
184
|
-
catch {
|
|
185
|
-
// Agent not found on network (e.g. server restarted with in-memory DB)
|
|
186
|
-
try {
|
|
187
|
-
await net.registerAgent(agent);
|
|
188
|
-
}
|
|
189
|
-
catch { /* best-effort */ }
|
|
190
|
-
}
|
|
191
|
-
ws.connect(agent.agent_id);
|
|
192
|
-
}
|
|
193
|
-
const restoredAgents = Object.values(s.agents).map((a) => ({
|
|
194
|
-
agent_id: a.agent_id,
|
|
195
|
-
name: a.name,
|
|
196
|
-
domains: a.domains,
|
|
197
|
-
tier: a.tier,
|
|
198
|
-
}));
|
|
306
|
+
// List agents available on disk (from previous sessions) — do NOT auto-restore.
|
|
307
|
+
// Each session must explicitly claim an agent via eacn3_claim_agent.
|
|
308
|
+
const availableAgents = state.listAvailableAgents();
|
|
199
309
|
return ok({
|
|
200
310
|
connected: true,
|
|
201
311
|
server_id: sid,
|
|
202
312
|
network_endpoint: endpoint,
|
|
203
313
|
fallback,
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
? "You have previously registered agents restored and reconnected. You can use them directly without re-registering. Call eacn3_list_my_agents() for full details."
|
|
314
|
+
available_agents: availableAgents,
|
|
315
|
+
hint: availableAgents.length > 0
|
|
316
|
+
? "Previous agents found on disk. Call eacn3_claim_agent(agent_id) to resume one, or eacn3_register_agent() to create a new one."
|
|
208
317
|
: "No previous agents found. Register a new agent with eacn3_register_agent().",
|
|
209
318
|
toolkit: {
|
|
210
319
|
workflow: {
|
|
@@ -212,9 +321,8 @@ server.tool("eacn3_connect", "Connect to the EACN3 network — this must be your
|
|
|
212
321
|
eacn3_get_events: "Drain all pending events at once (bulk alternative to next).",
|
|
213
322
|
},
|
|
214
323
|
team: {
|
|
215
|
-
eacn3_team_setup: "Form a team of agents around a shared git repo
|
|
216
|
-
eacn3_team_status: "Check team formation progress — which
|
|
217
|
-
eacn3_team_set_branch: "Record your git branch name for a team after creating it.",
|
|
324
|
+
eacn3_team_setup: "Form a team of agents around a shared git repo via ACK message exchange.",
|
|
325
|
+
eacn3_team_status: "Check team formation progress — which ACKs are exchanged, which peer branches are known.",
|
|
218
326
|
},
|
|
219
327
|
task: {
|
|
220
328
|
eacn3_create_task: "Publish a task (supports team_id to auto-inject team preamble).",
|
|
@@ -227,16 +335,16 @@ server.tool("eacn3_connect", "Connect to the EACN3 network — this must be your
|
|
|
227
335
|
});
|
|
228
336
|
});
|
|
229
337
|
// #2 eacn3_disconnect
|
|
230
|
-
server.tool("eacn3_disconnect", "Disconnect from the EACN3 network
|
|
338
|
+
server.tool("eacn3_disconnect", "Disconnect from the EACN3 network. Requires: eacn3_connect first. Side effects: active tasks will timeout and hurt reputation. Server identity is preserved — on next eacn3_connect you can claim your agent back via eacn3_claim_agent. Returns {disconnected: true}. Only call at end of session.", {}, async () => {
|
|
231
339
|
stopHeartbeat();
|
|
232
|
-
|
|
340
|
+
transport.disconnectAll();
|
|
233
341
|
// Do NOT call unregisterServer — it cascade-deletes all agents on the network side.
|
|
234
342
|
// We only go offline; identity is preserved for reconnection.
|
|
235
343
|
const s = state.getState();
|
|
236
|
-
|
|
344
|
+
// Don't write server.json — other sessions may still be using this server.
|
|
345
|
+
// Just clean up this session's in-memory state.
|
|
346
|
+
if (s.server_card)
|
|
237
347
|
s.server_card.status = "offline";
|
|
238
|
-
}
|
|
239
|
-
state.save();
|
|
240
348
|
return ok({ disconnected: true });
|
|
241
349
|
});
|
|
242
350
|
// #3 eacn3_heartbeat
|
|
@@ -266,8 +374,45 @@ server.tool("eacn3_server_info", "Get current server connection state, including
|
|
|
266
374
|
});
|
|
267
375
|
});
|
|
268
376
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
269
|
-
// Agent Management (
|
|
377
|
+
// Agent Management (8)
|
|
270
378
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
379
|
+
// #4b eacn3_claim_agent
|
|
380
|
+
server.tool("eacn3_claim_agent", "Claim a previously registered agent from disk into this session. Use this to resume an agent listed in available_agents from eacn3_connect. The agent is re-registered on the network and event transport is started. Only one agent per session.", {
|
|
381
|
+
agent_id: z.string().describe("ID of the agent to claim (from available_agents)"),
|
|
382
|
+
}, async (params) => {
|
|
383
|
+
logToolCall("eacn3_claim_agent", params);
|
|
384
|
+
if (state.listAgents().length > 0) {
|
|
385
|
+
return err("This session already has an agent. Only one agent per session.");
|
|
386
|
+
}
|
|
387
|
+
const agent = state.claimAgent(params.agent_id);
|
|
388
|
+
if (!agent) {
|
|
389
|
+
return err(`Agent ${params.agent_id} not found on disk. Use eacn3_register_agent to create a new one.`);
|
|
390
|
+
}
|
|
391
|
+
// Re-register on network with current session's server_id
|
|
392
|
+
const s = state.getState();
|
|
393
|
+
const oldServerId = agent.server_id;
|
|
394
|
+
if (s.server_card)
|
|
395
|
+
agent.server_id = s.server_card.server_id;
|
|
396
|
+
try {
|
|
397
|
+
await net.registerAgent(agent);
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
agent.server_id = oldServerId; // Revert on failure to stay consistent with network
|
|
401
|
+
}
|
|
402
|
+
// Start event transport
|
|
403
|
+
transport.connect(agent.agent_id);
|
|
404
|
+
// Initialize reverse control
|
|
405
|
+
rc.configure(agent.agent_id);
|
|
406
|
+
state.save();
|
|
407
|
+
logToolResult("eacn3_claim_agent", true, agent.agent_id);
|
|
408
|
+
return ok({
|
|
409
|
+
claimed: true,
|
|
410
|
+
agent_id: agent.agent_id,
|
|
411
|
+
name: agent.name,
|
|
412
|
+
domains: agent.domains,
|
|
413
|
+
tier: agent.tier,
|
|
414
|
+
});
|
|
415
|
+
});
|
|
271
416
|
// #5 eacn3_register_agent
|
|
272
417
|
// Inlines: adapter (AgentCard assembly) + registry (validate + persist + DHT)
|
|
273
418
|
server.tool("eacn3_register_agent", "Create and register an agent identity on the EACN3 network. Requires: eacn3_connect first. Assembles an AgentCard, registers it with the network, persists it locally, and starts polling for push events (task_broadcast, subtask_completed, etc.). Returns {agent_id, seeds, domains}. Domains control which task broadcasts you receive — be specific (e.g. 'python-coding' not 'coding').", {
|
|
@@ -334,8 +479,8 @@ server.tool("eacn3_register_agent", "Create and register an agent identity on th
|
|
|
334
479
|
const res = await net.registerAgent(card);
|
|
335
480
|
// Persist locally
|
|
336
481
|
state.addAgent(card);
|
|
337
|
-
//
|
|
338
|
-
|
|
482
|
+
// Register agent for on-demand event fetching
|
|
483
|
+
transport.connect(agentId);
|
|
339
484
|
// Configure reverse control for this agent
|
|
340
485
|
if (params.reverse_control?.enabled !== false) {
|
|
341
486
|
const rcPolicies = {};
|
|
@@ -367,9 +512,8 @@ server.tool("eacn3_register_agent", "Create and register an agent identity on th
|
|
|
367
512
|
eacn3_get_events: "Drain all pending events at once (bulk alternative to next).",
|
|
368
513
|
},
|
|
369
514
|
team: {
|
|
370
|
-
eacn3_team_setup: "Form a team of agents around a shared git repo
|
|
371
|
-
eacn3_team_status: "Check team formation progress — which
|
|
372
|
-
eacn3_team_set_branch: "Record your git branch name for a team after creating it.",
|
|
515
|
+
eacn3_team_setup: "Form a team of agents around a shared git repo via ACK message exchange.",
|
|
516
|
+
eacn3_team_status: "Check team formation progress — which ACKs are exchanged, which peer branches are known.",
|
|
373
517
|
},
|
|
374
518
|
task: {
|
|
375
519
|
eacn3_create_task: "Publish a task (supports team_id to auto-inject team preamble).",
|
|
@@ -425,11 +569,11 @@ server.tool("eacn3_update_agent", "Update a registered agent's mutable fields: n
|
|
|
425
569
|
return ok({ updated: true, agent_id, ...res });
|
|
426
570
|
});
|
|
427
571
|
// #8 eacn3_unregister_agent
|
|
428
|
-
server.tool("eacn3_unregister_agent", "Remove an agent from the network
|
|
572
|
+
server.tool("eacn3_unregister_agent", "Remove an agent from the network. Side effects: deletes agent from local state. Active tasks assigned to this agent will timeout and hurt reputation. Returns {unregistered: true, agent_id}.", {
|
|
429
573
|
agent_id: z.string(),
|
|
430
574
|
}, async (params) => {
|
|
431
575
|
const res = await net.unregisterAgent(params.agent_id);
|
|
432
|
-
|
|
576
|
+
transport.disconnect(params.agent_id);
|
|
433
577
|
rc.unconfigure(params.agent_id);
|
|
434
578
|
state.removeAgent(params.agent_id);
|
|
435
579
|
// Stop A2A server if no agents remain
|
|
@@ -439,7 +583,7 @@ server.tool("eacn3_unregister_agent", "Remove an agent from the network and stop
|
|
|
439
583
|
return ok({ unregistered: true, agent_id: params.agent_id, ...res });
|
|
440
584
|
});
|
|
441
585
|
// #9 eacn3_list_my_agents
|
|
442
|
-
server.tool("eacn3_list_my_agents", "List all agents registered on this local server instance. Returns {count, agents[]} where each agent includes agent_id, name, domains, tier, and
|
|
586
|
+
server.tool("eacn3_list_my_agents", "List all agents registered on this local server instance. Returns {count, agents[]} where each agent includes agent_id, name, domains, tier, and registered status. No network call — reads local state only. Use to check which agents are active.", {}, async () => {
|
|
443
587
|
const agents = state.listAgents();
|
|
444
588
|
return ok({
|
|
445
589
|
count: agents.length,
|
|
@@ -447,8 +591,8 @@ server.tool("eacn3_list_my_agents", "List all agents registered on this local se
|
|
|
447
591
|
agent_id: a.agent_id,
|
|
448
592
|
name: a.name,
|
|
449
593
|
domains: a.domains,
|
|
450
|
-
connected:
|
|
451
|
-
transport:
|
|
594
|
+
connected: transport.isConnected(a.agent_id),
|
|
595
|
+
transport: transport.getTransportStatus(a.agent_id),
|
|
452
596
|
})),
|
|
453
597
|
});
|
|
454
598
|
});
|
|
@@ -579,7 +723,7 @@ server.tool("eacn3_create_task", "Publish a new task to the EACN3 network for ot
|
|
|
579
723
|
].filter(Boolean).join("\n");
|
|
580
724
|
finalDescription = `${preamble}\n${params.description}`;
|
|
581
725
|
}
|
|
582
|
-
const task = await net.createTask({
|
|
726
|
+
const task = await withAutoReconnect(() => net.createTask({
|
|
583
727
|
task_id: taskId,
|
|
584
728
|
initiator_id: initiatorId,
|
|
585
729
|
content: {
|
|
@@ -594,7 +738,7 @@ server.tool("eacn3_create_task", "Publish a new task to the EACN3 network for ot
|
|
|
594
738
|
human_contact: params.human_contact,
|
|
595
739
|
level: params.level ?? "general",
|
|
596
740
|
invited_agent_ids: params.invited_agent_ids,
|
|
597
|
-
});
|
|
741
|
+
}));
|
|
598
742
|
// Track locally
|
|
599
743
|
state.updateTask({
|
|
600
744
|
task_id: taskId,
|
|
@@ -605,6 +749,11 @@ server.tool("eacn3_create_task", "Publish a new task to the EACN3 network for ot
|
|
|
605
749
|
description_summary: params.description.slice(0, 100),
|
|
606
750
|
created_at: new Date().toISOString(),
|
|
607
751
|
});
|
|
752
|
+
// If team task: reply to pending reverse handshakes with branch + task details
|
|
753
|
+
if (activeTeam) {
|
|
754
|
+
const taskSummary = { task_id: taskId, description: params.description.slice(0, 500) };
|
|
755
|
+
await replyPendingHandshakes(initiatorId, activeTeam, taskSummary);
|
|
756
|
+
}
|
|
608
757
|
return ok({
|
|
609
758
|
task_id: taskId,
|
|
610
759
|
status: task.status,
|
|
@@ -629,25 +778,6 @@ server.tool("eacn3_select_result", "Pick the winning result for a task, triggeri
|
|
|
629
778
|
}, async (params) => {
|
|
630
779
|
const initiatorId = resolveAgentId(params.initiator_id);
|
|
631
780
|
const res = await net.selectResult(params.task_id, initiatorId, params.agent_id);
|
|
632
|
-
// If this was a team handshake task, extract peer branch and update local team state
|
|
633
|
-
const teams = state.getTeamsForAgent(initiatorId);
|
|
634
|
-
for (const team of teams) {
|
|
635
|
-
const handshakeTarget = Object.entries(team.handshake_out).find(([, taskId]) => taskId === params.task_id);
|
|
636
|
-
if (handshakeTarget) {
|
|
637
|
-
// Fetch the result to get the branch name
|
|
638
|
-
try {
|
|
639
|
-
const taskData = await net.getTask(params.task_id);
|
|
640
|
-
const results = taskData?.results ?? [];
|
|
641
|
-
const winnerResult = results.find((r) => r.agent_id === params.agent_id);
|
|
642
|
-
const branch = winnerResult?.content?.branch;
|
|
643
|
-
if (branch) {
|
|
644
|
-
state.updateTeamPeerBranch(team.team_id, handshakeTarget[0], branch);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
catch { /* best effort */ }
|
|
648
|
-
break;
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
781
|
return ok(res);
|
|
652
782
|
});
|
|
653
783
|
// #19 eacn3_close_task
|
|
@@ -763,7 +893,7 @@ server.tool("eacn3_submit_bid", "Bid on an open task by specifying your confiden
|
|
|
763
893
|
const agentId = resolveAgentId(params.agent_id);
|
|
764
894
|
// Tier/level filtering and invite bypass are handled server-side in matcher.check_bid().
|
|
765
895
|
// No client-side pre-flight — the network returns "rejected" with reason for tier mismatches.
|
|
766
|
-
const res = await net.submitBid(params.task_id, agentId, params.confidence, params.price);
|
|
896
|
+
const res = await withAutoReconnect(() => net.submitBid(params.task_id, agentId, params.confidence, params.price));
|
|
767
897
|
// Track locally if not rejected (status could be "executing", "waiting_execution", etc.)
|
|
768
898
|
if (res.status && res.status !== "rejected") {
|
|
769
899
|
state.updateTask({
|
|
@@ -786,7 +916,7 @@ server.tool("eacn3_submit_result", "Submit your completed work for a task you ar
|
|
|
786
916
|
agent_id: z.string().optional().describe("Executor agent ID (auto-injected if omitted)"),
|
|
787
917
|
}, async (params) => {
|
|
788
918
|
const agentId = resolveAgentId(params.agent_id);
|
|
789
|
-
const res = await net.submitResult(params.task_id, agentId, params.content);
|
|
919
|
+
const res = await withAutoReconnect(() => net.submitResult(params.task_id, agentId, params.content));
|
|
790
920
|
// Auto-report reputation event (what logger used to do)
|
|
791
921
|
try {
|
|
792
922
|
await net.reportEvent(agentId, "task_completed");
|
|
@@ -802,7 +932,7 @@ server.tool("eacn3_reject_task", "Abandon a task you accepted, freeing your exec
|
|
|
802
932
|
agent_id: z.string().optional().describe("Executor agent ID (auto-injected if omitted)"),
|
|
803
933
|
}, async (params) => {
|
|
804
934
|
const agentId = resolveAgentId(params.agent_id);
|
|
805
|
-
const res = await net.rejectTask(params.task_id, agentId, params.reason);
|
|
935
|
+
const res = await withAutoReconnect(() => net.rejectTask(params.task_id, agentId, params.reason));
|
|
806
936
|
// Auto-report reputation event
|
|
807
937
|
try {
|
|
808
938
|
await net.reportEvent(agentId, "task_rejected");
|
|
@@ -821,7 +951,7 @@ server.tool("eacn3_create_subtask", "Delegate part of your work by creating a ch
|
|
|
821
951
|
initiator_id: z.string().optional().describe("Agent ID of the executor creating the subtask (auto-injected if omitted)"),
|
|
822
952
|
}, async (params) => {
|
|
823
953
|
const initiatorId = resolveAgentId(params.initiator_id);
|
|
824
|
-
const task = await net.createSubtask(params.parent_task_id, initiatorId, { description: params.description }, params.domains, params.budget, params.deadline, params.level);
|
|
954
|
+
const task = await withAutoReconnect(() => net.createSubtask(params.parent_task_id, initiatorId, { description: params.description }, params.domains, params.budget, params.deadline, params.level));
|
|
825
955
|
return ok({
|
|
826
956
|
subtask_id: task.id,
|
|
827
957
|
parent_task_id: params.parent_task_id,
|
|
@@ -878,6 +1008,7 @@ server.tool("eacn3_send_message", "Send a direct agent-to-agent message. Deliver
|
|
|
878
1008
|
from: senderId,
|
|
879
1009
|
content: params.content,
|
|
880
1010
|
}),
|
|
1011
|
+
signal: AbortSignal.timeout(10_000),
|
|
881
1012
|
});
|
|
882
1013
|
if (res.ok) {
|
|
883
1014
|
return ok({ sent: true, to: targetId, from: senderId, method: "a2a_direct" });
|
|
@@ -971,19 +1102,21 @@ server.tool("eacn3_list_sessions", "List all agents you have active message sess
|
|
|
971
1102
|
});
|
|
972
1103
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
973
1104
|
// #34 eacn3_get_events
|
|
974
|
-
server.tool("eacn3_get_events", "
|
|
1105
|
+
server.tool("eacn3_get_events", "Fetch pending events from the network for a specific agent, plus any locally buffered synthetic events. Returns {count, events[], reverse_control} where event types include: task_broadcast, bid_request_confirmation, bid_result, discussion_update, subtask_completed, task_collected, task_timeout, adjudication_task, direct_message. With reverse_control enabled, high-priority events may already have been handled via LLM sampling — check reverse_control.status for details.", {
|
|
975
1106
|
agent_id: z.string().optional().describe("Agent ID to drain events for (auto-injected if omitted)"),
|
|
976
1107
|
}, async (params) => {
|
|
977
1108
|
const agentId = resolveAgentId(params.agent_id);
|
|
978
|
-
const
|
|
1109
|
+
const networkEvents = await transport.fetchEvents(agentId, 0);
|
|
1110
|
+
const localEvents = state.drainEvents(agentId);
|
|
1111
|
+
const events = [...networkEvents, ...localEvents].filter((e) => !e._handled);
|
|
979
1112
|
return ok({
|
|
980
1113
|
count: events.length,
|
|
981
1114
|
events,
|
|
982
1115
|
reverse_control: rc.getStatus(),
|
|
983
1116
|
});
|
|
984
1117
|
});
|
|
985
|
-
// #39 eacn3_await_events — long-polling
|
|
986
|
-
server.tool("eacn3_await_events", "
|
|
1118
|
+
// #39 eacn3_await_events — on-demand long-polling
|
|
1119
|
+
server.tool("eacn3_await_events", "Fetch events from the network with a configurable wait time. First checks locally buffered synthetic events, then does a single long-poll to the network. Returns {event, suggested_action, suggested_tool, suggested_params, urgency} per event, or {timeout: true}. Prefer this over eacn3_get_events for reactive agent loops.", {
|
|
987
1120
|
agent_id: z.string().optional().describe("Agent ID to await events for (auto-injected if omitted)"),
|
|
988
1121
|
timeout_seconds: z.number().optional().describe("Max seconds to wait (1-120). Default 30."),
|
|
989
1122
|
event_types: z.array(z.string()).optional().describe("Only return for these event types. Default: all."),
|
|
@@ -991,27 +1124,29 @@ server.tool("eacn3_await_events", "Block until a network event arrives or timeou
|
|
|
991
1124
|
const agentId = resolveAgentId(params.agent_id);
|
|
992
1125
|
const timeoutSec = Math.min(Math.max(params.timeout_seconds ?? 30, 1), 120);
|
|
993
1126
|
const filterTypes = params.event_types;
|
|
994
|
-
// Check
|
|
995
|
-
const immediate = drainMatchingEvents(agentId, filterTypes);
|
|
1127
|
+
// Check locally buffered synthetic events first
|
|
1128
|
+
const immediate = drainMatchingEvents(agentId, filterTypes).filter((e) => !e._handled);
|
|
996
1129
|
if (immediate.length > 0) {
|
|
997
1130
|
return ok(buildAwaitResponse(immediate));
|
|
998
1131
|
}
|
|
999
|
-
//
|
|
1000
|
-
const
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1132
|
+
// Single long-poll to network with the agent's requested timeout
|
|
1133
|
+
const networkEvents = await transport.fetchEvents(agentId, timeoutSec);
|
|
1134
|
+
const localAfter = drainMatchingEvents(agentId, filterTypes);
|
|
1135
|
+
const all = [...networkEvents, ...localAfter].filter((e) => !e._handled);
|
|
1136
|
+
// Filter by event types if specified
|
|
1137
|
+
const filtered = filterTypes && filterTypes.length > 0
|
|
1138
|
+
? all.filter((e) => filterTypes.includes(e.type))
|
|
1139
|
+
: all;
|
|
1140
|
+
// Put back non-matching events
|
|
1141
|
+
if (filterTypes && filterTypes.length > 0) {
|
|
1142
|
+
const remaining = all.filter((e) => !filterTypes.includes(e.type));
|
|
1143
|
+
if (remaining.length > 0)
|
|
1144
|
+
state.pushEvents(agentId, remaining);
|
|
1145
|
+
}
|
|
1146
|
+
if (filtered.length === 0) {
|
|
1012
1147
|
return ok({ timeout: true, waited_seconds: timeoutSec, hint: "No events arrived. Call again to keep waiting, or proceed with other work." });
|
|
1013
1148
|
}
|
|
1014
|
-
return ok(buildAwaitResponse(
|
|
1149
|
+
return ok(buildAwaitResponse(filtered));
|
|
1015
1150
|
});
|
|
1016
1151
|
// #41 eacn3_next — single-event non-blocking poll
|
|
1017
1152
|
const URGENCY_ORDER = {
|
|
@@ -1030,22 +1165,6 @@ function buildNextAction(event) {
|
|
|
1030
1165
|
const payload = event.payload;
|
|
1031
1166
|
switch (event.type) {
|
|
1032
1167
|
case "task_broadcast": {
|
|
1033
|
-
// Detect team handshake tasks
|
|
1034
|
-
const desc = String(payload?.description ?? "");
|
|
1035
|
-
if (desc.startsWith("Team handshake:") || payload?.budget === 0) {
|
|
1036
|
-
// Check if this looks like a handshake (0-budget, team-coordination domain)
|
|
1037
|
-
const domains = payload.domains ?? [];
|
|
1038
|
-
if (domains.includes("team-coordination")) {
|
|
1039
|
-
return {
|
|
1040
|
-
action: "handshake",
|
|
1041
|
-
description: `Team handshake from another agent on task ${event.task_id}. ` +
|
|
1042
|
-
`Bid with price=0 confidence=1, then submit_result with your branch name: ` +
|
|
1043
|
-
`{branch: "agent/your-id", _handshake_ack: true, team_id: "..."}.`,
|
|
1044
|
-
tool: "eacn3_submit_bid",
|
|
1045
|
-
params: { task_id: event.task_id, confidence: 1, price: 0 },
|
|
1046
|
-
};
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
1168
|
return {
|
|
1050
1169
|
action: "bid",
|
|
1051
1170
|
description: `New task [${(payload.domains ?? []).join(", ")}] budget=${payload.budget ?? "?"}. Evaluate and bid.`,
|
|
@@ -1125,7 +1244,10 @@ server.tool("eacn3_next", "Non-blocking single-step work dispatcher: returns the
|
|
|
1125
1244
|
agent_id: z.string().optional().describe("Agent ID (auto-injected if omitted)"),
|
|
1126
1245
|
}, async (params) => {
|
|
1127
1246
|
const agentId = resolveAgentId(params.agent_id);
|
|
1128
|
-
|
|
1247
|
+
// Fetch from network (non-blocking) + drain local synthetic events
|
|
1248
|
+
const networkEvents = await transport.fetchEvents(agentId, 0);
|
|
1249
|
+
const localEvents = state.drainEvents(agentId);
|
|
1250
|
+
const events = [...networkEvents, ...localEvents].filter((e) => !e._handled);
|
|
1129
1251
|
if (events.length === 0) {
|
|
1130
1252
|
// Build context-aware prompts based on agent's current task state
|
|
1131
1253
|
const tasks = Object.values(state.getState().local_tasks).filter((t) => t.agent_id === agentId);
|
|
@@ -1186,128 +1308,95 @@ server.tool("eacn3_next", "Non-blocking single-step work dispatcher: returns the
|
|
|
1186
1308
|
...next,
|
|
1187
1309
|
});
|
|
1188
1310
|
});
|
|
1189
|
-
// #42 eacn3_team_setup —
|
|
1190
|
-
server.tool("eacn3_team_setup", "
|
|
1191
|
-
"Creates 0-budget
|
|
1192
|
-
"
|
|
1193
|
-
"
|
|
1311
|
+
// #42 eacn3_team_setup — task-based team handshake (fully automatic)
|
|
1312
|
+
server.tool("eacn3_team_setup", "Form a team of agents around a shared git repo. " +
|
|
1313
|
+
"Creates handshake tasks (0-budget, 30-min deadline) to exchange branch info with each peer. " +
|
|
1314
|
+
"Peers auto-bid and auto-reply; handshake is purely for branch exchange. " +
|
|
1315
|
+
"After team is ready, use eacn3_create_task with team_id to publish work for the team.", {
|
|
1194
1316
|
agent_ids: z.array(z.string()).min(2).describe("Agent IDs to form a team"),
|
|
1195
1317
|
git_repo: z.string().describe("Git repo URL for recording operations"),
|
|
1196
|
-
|
|
1318
|
+
my_branch: z.string().describe("This agent's operation branch name"),
|
|
1197
1319
|
}, async (params) => {
|
|
1198
|
-
|
|
1199
|
-
const
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1320
|
+
// Only the calling agent creates outgoing handshakes — peers join via autoHandshakeRespond
|
|
1321
|
+
const myId = resolveAgentId(undefined);
|
|
1322
|
+
if (!state.getAgent(myId)) {
|
|
1323
|
+
return err(`Agent ${myId} is not registered on this server`);
|
|
1324
|
+
}
|
|
1325
|
+
if (!params.agent_ids.includes(myId)) {
|
|
1326
|
+
return err(`Your agent ID ${myId} must be included in agent_ids`);
|
|
1204
1327
|
}
|
|
1205
1328
|
const teamId = `team-${Date.now().toString(36)}`;
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1329
|
+
const peers = params.agent_ids.filter((id) => id !== myId);
|
|
1330
|
+
const teamInfo = {
|
|
1331
|
+
team_id: teamId,
|
|
1332
|
+
git_repo: params.git_repo,
|
|
1333
|
+
agent_ids: params.agent_ids,
|
|
1334
|
+
my_agent_id: myId,
|
|
1335
|
+
my_branch: params.my_branch,
|
|
1336
|
+
peer_branches: {},
|
|
1337
|
+
ack_out: {},
|
|
1338
|
+
ack_in: {},
|
|
1339
|
+
is_initiator: true,
|
|
1340
|
+
status: "forming",
|
|
1341
|
+
};
|
|
1342
|
+
const tasksCreated = [];
|
|
1343
|
+
const failed = [];
|
|
1344
|
+
for (const peerId of peers) {
|
|
1345
|
+
try {
|
|
1346
|
+
const taskId = `t-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
1347
|
+
const handshakeDeadline = new Date(Date.now() + 30 * 60 * 1000).toISOString();
|
|
1348
|
+
const task = await net.createTask({
|
|
1349
|
+
task_id: taskId,
|
|
1350
|
+
initiator_id: myId,
|
|
1351
|
+
content: { description: `Team handshake: ${myId} → ${peerId} [team=${teamId}] [repo=${params.git_repo}] [members=${params.agent_ids.join(",")}]` },
|
|
1352
|
+
domains: ["team-coordination"],
|
|
1353
|
+
budget: 0,
|
|
1354
|
+
deadline: handshakeDeadline,
|
|
1355
|
+
max_concurrent_bidders: 1,
|
|
1356
|
+
max_depth: 0,
|
|
1357
|
+
invited_agent_ids: [peerId],
|
|
1358
|
+
});
|
|
1359
|
+
teamInfo.ack_out[peerId] = task.id;
|
|
1360
|
+
tasksCreated.push(task.id);
|
|
1361
|
+
state.updateTask({
|
|
1362
|
+
task_id: task.id,
|
|
1363
|
+
agent_id: myId,
|
|
1364
|
+
role: "initiator",
|
|
1365
|
+
status: task.status,
|
|
1366
|
+
domains: ["team-coordination"],
|
|
1367
|
+
description_summary: `Handshake → ${peerId}`,
|
|
1368
|
+
created_at: new Date().toISOString(),
|
|
1369
|
+
});
|
|
1239
1370
|
}
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
for (const peerId of peers) {
|
|
1243
|
-
try {
|
|
1244
|
-
const taskId = `t-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
1245
|
-
const content = {
|
|
1246
|
-
_handshake: true,
|
|
1247
|
-
team_id: teamId,
|
|
1248
|
-
git_repo: params.git_repo,
|
|
1249
|
-
from_agent: myId,
|
|
1250
|
-
team_members: params.agent_ids,
|
|
1251
|
-
};
|
|
1252
|
-
const task = await net.createTask({
|
|
1253
|
-
task_id: taskId,
|
|
1254
|
-
initiator_id: myId,
|
|
1255
|
-
content: { description: `Team handshake: ${myId} → ${peerId}` },
|
|
1256
|
-
domains: ["team-coordination"],
|
|
1257
|
-
budget: 0,
|
|
1258
|
-
max_concurrent_bidders: 1,
|
|
1259
|
-
max_depth: 0,
|
|
1260
|
-
invited_agent_ids: [peerId],
|
|
1261
|
-
});
|
|
1262
|
-
// Update task content with handshake marker via discussions
|
|
1263
|
-
try {
|
|
1264
|
-
await net.updateDiscussions(task.id, myId, JSON.stringify(content));
|
|
1265
|
-
}
|
|
1266
|
-
catch { /* content is in description as fallback */ }
|
|
1267
|
-
teamInfo.handshake_out[peerId] = task.id;
|
|
1268
|
-
handshakesCreated.push(task.id);
|
|
1269
|
-
state.updateTask({
|
|
1270
|
-
task_id: task.id,
|
|
1271
|
-
agent_id: myId,
|
|
1272
|
-
role: "initiator",
|
|
1273
|
-
status: task.status,
|
|
1274
|
-
domains: ["team-coordination"],
|
|
1275
|
-
description_summary: `Handshake → ${peerId}`,
|
|
1276
|
-
created_at: new Date().toISOString(),
|
|
1277
|
-
});
|
|
1278
|
-
}
|
|
1279
|
-
catch (e) {
|
|
1280
|
-
handshakesCreated.push(`FAILED(${peerId}): ${e.message ?? e}`);
|
|
1281
|
-
}
|
|
1371
|
+
catch (e) {
|
|
1372
|
+
failed.push(`${peerId}: ${e.message ?? e}`);
|
|
1282
1373
|
}
|
|
1283
|
-
state.addTeam(teamInfo);
|
|
1284
|
-
results.push({ agent_id: myId, handshakes_created: handshakesCreated });
|
|
1285
1374
|
}
|
|
1375
|
+
state.addTeam(teamInfo);
|
|
1286
1376
|
return ok({
|
|
1287
1377
|
team_id: teamId,
|
|
1288
1378
|
git_repo: params.git_repo,
|
|
1289
1379
|
agent_ids: params.agent_ids,
|
|
1290
|
-
|
|
1291
|
-
|
|
1380
|
+
my_agent_id: myId,
|
|
1381
|
+
my_branch: params.my_branch,
|
|
1382
|
+
tasks_created: tasksCreated,
|
|
1383
|
+
failed,
|
|
1292
1384
|
next_steps: [
|
|
1293
|
-
"
|
|
1294
|
-
"
|
|
1295
|
-
"bid with price=0 and confidence=1, then submit_result with {branch: 'your-branch-name', _handshake_ack: true, team_id: '...'}.",
|
|
1296
|
-
"When your handshake tasks get results, select_result to complete the handshake.",
|
|
1297
|
-
"Team is ready when all peer branches are known.",
|
|
1385
|
+
"Handshake tasks are auto-processed — peers auto-bid and reply, results are auto-selected.",
|
|
1386
|
+
"Call eacn3_team_status to check progress. Use eacn3_team_retry_ack if a peer is unresponsive.",
|
|
1298
1387
|
],
|
|
1299
1388
|
});
|
|
1300
1389
|
});
|
|
1301
1390
|
// #43 eacn3_team_status — check team formation progress
|
|
1302
|
-
server.tool("eacn3_team_status", "Check
|
|
1391
|
+
server.tool("eacn3_team_status", "Check team formation progress: which handshake tasks are complete, which peer branches are known, and whether the team is ready. If peers are unresponsive, use eacn3_team_retry_ack.", {
|
|
1303
1392
|
team_id: z.string().describe("Team ID from eacn3_team_setup"),
|
|
1304
1393
|
}, async (params) => {
|
|
1305
1394
|
const team = state.getTeam(params.team_id);
|
|
1306
1395
|
if (!team)
|
|
1307
1396
|
return err(`Team ${params.team_id} not found`);
|
|
1308
1397
|
const peers = team.agent_ids.filter((id) => id !== team.my_agent_id);
|
|
1309
|
-
const
|
|
1310
|
-
const
|
|
1398
|
+
const connected = peers.filter((id) => id in team.peer_branches);
|
|
1399
|
+
const pending = peers.filter((id) => !(id in team.peer_branches));
|
|
1311
1400
|
return ok({
|
|
1312
1401
|
team_id: team.team_id,
|
|
1313
1402
|
git_repo: team.git_repo,
|
|
@@ -1315,21 +1404,45 @@ server.tool("eacn3_team_status", "Check the current status of a team: which hand
|
|
|
1315
1404
|
my_agent_id: team.my_agent_id,
|
|
1316
1405
|
my_branch: team.my_branch ?? null,
|
|
1317
1406
|
peer_branches: team.peer_branches,
|
|
1318
|
-
|
|
1319
|
-
|
|
1407
|
+
ack_out: team.ack_out,
|
|
1408
|
+
ack_in: team.ack_in,
|
|
1409
|
+
connected,
|
|
1410
|
+
pending,
|
|
1320
1411
|
ready: team.status === "ready",
|
|
1321
1412
|
});
|
|
1322
1413
|
});
|
|
1323
|
-
// #
|
|
1324
|
-
server.tool("
|
|
1414
|
+
// #45 eacn3_team_retry_ack — re-create handshake task for unresponsive peer
|
|
1415
|
+
server.tool("eacn3_team_retry_ack", "Re-create a handshake task for a specific peer who hasn't responded. Use when eacn3_team_status shows a peer in 'pending'.", {
|
|
1325
1416
|
team_id: z.string().describe("Team ID"),
|
|
1326
|
-
|
|
1417
|
+
peer_id: z.string().describe("Agent ID of the unresponsive peer"),
|
|
1327
1418
|
}, async (params) => {
|
|
1328
1419
|
const team = state.getTeam(params.team_id);
|
|
1329
1420
|
if (!team)
|
|
1330
1421
|
return err(`Team ${params.team_id} not found`);
|
|
1331
|
-
|
|
1332
|
-
|
|
1422
|
+
if (!team.agent_ids.includes(params.peer_id)) {
|
|
1423
|
+
return err(`${params.peer_id} is not a member of team ${params.team_id}`);
|
|
1424
|
+
}
|
|
1425
|
+
try {
|
|
1426
|
+
const taskId = `t-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
1427
|
+
const handshakeDeadline = new Date(Date.now() + 30 * 60 * 1000).toISOString();
|
|
1428
|
+
const task = await net.createTask({
|
|
1429
|
+
task_id: taskId,
|
|
1430
|
+
initiator_id: team.my_agent_id,
|
|
1431
|
+
content: { description: `Team handshake: ${team.my_agent_id} → ${params.peer_id} [team=${team.team_id}] [repo=${team.git_repo}] [members=${team.agent_ids.join(",")}]` },
|
|
1432
|
+
domains: ["team-coordination"],
|
|
1433
|
+
budget: 0,
|
|
1434
|
+
deadline: handshakeDeadline,
|
|
1435
|
+
max_concurrent_bidders: 1,
|
|
1436
|
+
max_depth: 0,
|
|
1437
|
+
invited_agent_ids: [params.peer_id],
|
|
1438
|
+
});
|
|
1439
|
+
team.ack_out[params.peer_id] = task.id;
|
|
1440
|
+
state.addTeam(team); // persist updated ack_out
|
|
1441
|
+
return ok({ team_id: team.team_id, peer_id: params.peer_id, task_id: task.id, message: "Handshake task re-created" });
|
|
1442
|
+
}
|
|
1443
|
+
catch (e) {
|
|
1444
|
+
return err(`Failed to create handshake task for ${params.peer_id}: ${e.message ?? e}`);
|
|
1445
|
+
}
|
|
1333
1446
|
});
|
|
1334
1447
|
// #40 eacn3_reverse_control_status
|
|
1335
1448
|
server.tool("eacn3_reverse_control_status", "Get the current status of the MCP reverse control engine. Shows whether sampling is available, which agents are configured, pending directive count, and rate limiting info. Use for debugging reverse control behavior.", {}, async () => {
|
|
@@ -1386,7 +1499,11 @@ function buildAwaitResponse(events) {
|
|
|
1386
1499
|
// WS Event Callbacks — auto-actions when events arrive
|
|
1387
1500
|
// ---------------------------------------------------------------------------
|
|
1388
1501
|
function registerEventCallbacks() {
|
|
1389
|
-
|
|
1502
|
+
transport.setEventCallback((agentId, event) => {
|
|
1503
|
+
// Skip if agent not claimed in this session — events are on-demand only,
|
|
1504
|
+
// but guard against edge cases.
|
|
1505
|
+
if (!state.getAgent(agentId))
|
|
1506
|
+
return;
|
|
1390
1507
|
const taskId = event.task_id;
|
|
1391
1508
|
// --- Reverse Control: try to handle event proactively ---
|
|
1392
1509
|
// This runs async; if it handles the event, it may take action
|
|
@@ -1427,12 +1544,39 @@ function registerEventCallbacks() {
|
|
|
1427
1544
|
// Bid exceeded budget — mark in local state for initiator to handle
|
|
1428
1545
|
// The event stays in the buffer for /eacn3-bounty to surface
|
|
1429
1546
|
break;
|
|
1430
|
-
case "task_broadcast":
|
|
1431
|
-
|
|
1432
|
-
|
|
1547
|
+
case "task_broadcast": {
|
|
1548
|
+
const bPayload = event.payload;
|
|
1549
|
+
const bDomains = bPayload?.domains ?? [];
|
|
1550
|
+
const bDesc = String(bPayload?.description ?? "");
|
|
1551
|
+
if (bDomains.includes("team-coordination") && bDesc.startsWith("Team handshake:")) {
|
|
1552
|
+
event._handled = true;
|
|
1553
|
+
autoHandshakeRespond(agentId, event).catch((e) => { console.error(`[handshake] autoHandshakeRespond failed for ${agentId}:`, e); });
|
|
1554
|
+
}
|
|
1555
|
+
else {
|
|
1556
|
+
autoBidEvaluate(agentId, event).catch(() => { });
|
|
1557
|
+
}
|
|
1558
|
+
break;
|
|
1559
|
+
}
|
|
1560
|
+
case "bid_result": {
|
|
1561
|
+
const brPayload = event.payload;
|
|
1562
|
+
if (brPayload?.accepted) {
|
|
1563
|
+
const match = state.findTeamByHandshakeTask(event.task_id);
|
|
1564
|
+
if (match && match.direction === "in") {
|
|
1565
|
+
event._handled = true;
|
|
1566
|
+
autoHandshakeSubmit(agentId, event).catch((e) => { console.error(`[handshake] autoHandshakeSubmit failed for ${agentId}:`, e); });
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
break;
|
|
1570
|
+
}
|
|
1571
|
+
case "result_submitted": {
|
|
1572
|
+
const match = state.findTeamByHandshakeTask(event.task_id);
|
|
1573
|
+
if (match && match.direction === "out") {
|
|
1574
|
+
event._handled = true;
|
|
1575
|
+
autoHandshakeSelect(agentId, event).catch((e) => { console.error(`[handshake] autoHandshakeSelect failed for ${agentId}:`, e); });
|
|
1576
|
+
}
|
|
1433
1577
|
break;
|
|
1578
|
+
}
|
|
1434
1579
|
case "direct_message": {
|
|
1435
|
-
// Another agent sent a direct message — store in session
|
|
1436
1580
|
const payload = event.payload;
|
|
1437
1581
|
const from = payload?.from;
|
|
1438
1582
|
const content = payload?.content;
|
|
@@ -1451,6 +1595,198 @@ function registerEventCallbacks() {
|
|
|
1451
1595
|
});
|
|
1452
1596
|
}
|
|
1453
1597
|
// ---------------------------------------------------------------------------
|
|
1598
|
+
// Team handshake auto-handling
|
|
1599
|
+
// ---------------------------------------------------------------------------
|
|
1600
|
+
/**
|
|
1601
|
+
* Auto-respond to an incoming handshake task_broadcast:
|
|
1602
|
+
* 1. Parse team info from description
|
|
1603
|
+
* 2. Create local team record if needed, auto-set branch to agent/{id}
|
|
1604
|
+
* 3. Auto-bid on the incoming task
|
|
1605
|
+
* 4. Create outgoing handshake tasks to all peers we haven't ACKed yet
|
|
1606
|
+
*/
|
|
1607
|
+
async function autoHandshakeRespond(agentId, event) {
|
|
1608
|
+
const payload = event.payload;
|
|
1609
|
+
const desc = String(payload?.description ?? "");
|
|
1610
|
+
const teamMatch = desc.match(/\[team=([^\]]+)\]/);
|
|
1611
|
+
const repoMatch = desc.match(/\[repo=([^\]]+)\]/);
|
|
1612
|
+
const membersMatch = desc.match(/\[members=([^\]]+)\]/);
|
|
1613
|
+
const fromMatch = desc.match(/^Team handshake:\s*(\S+)/);
|
|
1614
|
+
if (!teamMatch)
|
|
1615
|
+
return;
|
|
1616
|
+
const teamId = teamMatch[1];
|
|
1617
|
+
const gitRepo = repoMatch?.[1] ?? "";
|
|
1618
|
+
const members = membersMatch?.[1]?.split(",") ?? [];
|
|
1619
|
+
const fromAgent = fromMatch?.[1] ?? "";
|
|
1620
|
+
// Ensure local team record exists
|
|
1621
|
+
let team = state.getTeamsForAgent(agentId).find((t) => t.team_id === teamId);
|
|
1622
|
+
if (!team) {
|
|
1623
|
+
const teamInfo = {
|
|
1624
|
+
team_id: teamId,
|
|
1625
|
+
git_repo: gitRepo,
|
|
1626
|
+
agent_ids: members,
|
|
1627
|
+
my_agent_id: agentId,
|
|
1628
|
+
my_branch: `agent/${agentId}`,
|
|
1629
|
+
peer_branches: {},
|
|
1630
|
+
ack_out: {},
|
|
1631
|
+
ack_in: {},
|
|
1632
|
+
status: "forming",
|
|
1633
|
+
};
|
|
1634
|
+
state.addTeam(teamInfo);
|
|
1635
|
+
team = state.getTeamsForAgent(agentId).find((t) => t.team_id === teamId);
|
|
1636
|
+
}
|
|
1637
|
+
// Auto-set branch if not yet set
|
|
1638
|
+
if (!team.my_branch) {
|
|
1639
|
+
state.setTeamBranch(teamId, `agent/${agentId}`);
|
|
1640
|
+
team.my_branch = `agent/${agentId}`;
|
|
1641
|
+
}
|
|
1642
|
+
// Record incoming handshake task
|
|
1643
|
+
state.recordAckIn(teamId, agentId, fromAgent, event.task_id);
|
|
1644
|
+
// If this agent is the team initiator (called eacn3_team_setup), DON'T auto-respond.
|
|
1645
|
+
// The initiator replies later via replyPendingHandshakes (called by create_task)
|
|
1646
|
+
// so it can include task details in the response.
|
|
1647
|
+
if (team.is_initiator)
|
|
1648
|
+
return;
|
|
1649
|
+
// Auto-bid on the incoming task
|
|
1650
|
+
try {
|
|
1651
|
+
await net.submitBid(event.task_id, agentId, 0, 1);
|
|
1652
|
+
}
|
|
1653
|
+
catch (e) {
|
|
1654
|
+
console.error(`[handshake] auto-bid failed for ${agentId} on task ${event.task_id}:`, e);
|
|
1655
|
+
return; // Can't proceed without a bid
|
|
1656
|
+
}
|
|
1657
|
+
// Create outgoing tasks to peers we haven't ACKed yet
|
|
1658
|
+
await createOutgoingHandshakes(team, agentId);
|
|
1659
|
+
}
|
|
1660
|
+
/**
|
|
1661
|
+
* Auto-submit result after handshake bid is accepted (bid_result event).
|
|
1662
|
+
* Refuses to submit if branch is not set.
|
|
1663
|
+
*/
|
|
1664
|
+
async function autoHandshakeSubmit(agentId, event) {
|
|
1665
|
+
const taskId = event.task_id;
|
|
1666
|
+
const match = state.findTeamByHandshakeTask(taskId);
|
|
1667
|
+
if (!match || match.direction !== "in")
|
|
1668
|
+
return;
|
|
1669
|
+
try {
|
|
1670
|
+
await net.submitResult(taskId, agentId, {
|
|
1671
|
+
_handshake_ack: true,
|
|
1672
|
+
team_id: match.team.team_id,
|
|
1673
|
+
branch: match.team.my_branch ?? `agent/${agentId}`,
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
catch (e) {
|
|
1677
|
+
console.error(`[handshake] auto-submit failed for ${agentId} on task ${taskId}:`, e);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Create outgoing handshake tasks to all peers we haven't ACKed yet.
|
|
1682
|
+
*/
|
|
1683
|
+
async function createOutgoingHandshakes(team, agentId) {
|
|
1684
|
+
const peers = team.agent_ids.filter((id) => id !== agentId);
|
|
1685
|
+
for (const peerId of peers) {
|
|
1686
|
+
if (team.ack_out[peerId])
|
|
1687
|
+
continue;
|
|
1688
|
+
try {
|
|
1689
|
+
const taskId = `t-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
1690
|
+
const handshakeDeadline = new Date(Date.now() + 30 * 60 * 1000).toISOString();
|
|
1691
|
+
const task = await net.createTask({
|
|
1692
|
+
task_id: taskId,
|
|
1693
|
+
initiator_id: agentId,
|
|
1694
|
+
content: { description: `Team handshake: ${agentId} → ${peerId} [team=${team.team_id}] [repo=${team.git_repo}] [members=${team.agent_ids.join(",")}]` },
|
|
1695
|
+
domains: ["team-coordination"],
|
|
1696
|
+
budget: 0,
|
|
1697
|
+
deadline: handshakeDeadline,
|
|
1698
|
+
max_concurrent_bidders: 1,
|
|
1699
|
+
max_depth: 0,
|
|
1700
|
+
invited_agent_ids: [peerId],
|
|
1701
|
+
});
|
|
1702
|
+
team.ack_out[peerId] = task.id;
|
|
1703
|
+
state.addTeam(team);
|
|
1704
|
+
}
|
|
1705
|
+
catch (e) {
|
|
1706
|
+
console.error(`[handshake] createOutgoingHandshake ${agentId} → ${peerId} failed:`, e);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* Auto-select result for handshake tasks when a result is submitted.
|
|
1712
|
+
* Only acts on tasks in our ack_out (i.e., tasks we created as initiator).
|
|
1713
|
+
*/
|
|
1714
|
+
async function autoHandshakeSelect(agentId, event) {
|
|
1715
|
+
const taskId = event.task_id;
|
|
1716
|
+
const match = state.findTeamByHandshakeTask(taskId);
|
|
1717
|
+
if (!match || match.direction !== "out")
|
|
1718
|
+
return; // Not our outgoing handshake
|
|
1719
|
+
const payload = event.payload;
|
|
1720
|
+
const resultAgentId = payload?.agent_id;
|
|
1721
|
+
if (!resultAgentId)
|
|
1722
|
+
return;
|
|
1723
|
+
// Auto-select the result
|
|
1724
|
+
try {
|
|
1725
|
+
await net.selectResult(taskId, agentId, resultAgentId);
|
|
1726
|
+
}
|
|
1727
|
+
catch (e) {
|
|
1728
|
+
console.error(`[handshake] auto-select failed for ${agentId} on task ${taskId}:`, e);
|
|
1729
|
+
}
|
|
1730
|
+
// Extract branch from the result
|
|
1731
|
+
try {
|
|
1732
|
+
const taskData = await net.getTask(taskId);
|
|
1733
|
+
const results = taskData?.results ?? [];
|
|
1734
|
+
const result = results.find((r) => r.agent_id === resultAgentId);
|
|
1735
|
+
const branch = result?.content?.branch;
|
|
1736
|
+
if (branch) {
|
|
1737
|
+
state.updateTeamPeerBranch(match.team.team_id, match.peerId, branch);
|
|
1738
|
+
}
|
|
1739
|
+
// If the reply carries a team task, buffer it as a synthetic event for the agent
|
|
1740
|
+
const teamTask = result?.content?.team_task;
|
|
1741
|
+
if (teamTask && teamTask.task_id) {
|
|
1742
|
+
state.pushEvents(agentId, [{
|
|
1743
|
+
msg_id: crypto.randomUUID().replace(/-/g, ""),
|
|
1744
|
+
type: "direct_message",
|
|
1745
|
+
task_id: teamTask.task_id,
|
|
1746
|
+
payload: {
|
|
1747
|
+
from: match.peerId,
|
|
1748
|
+
content: JSON.stringify({ _team_task: true, ...teamTask }),
|
|
1749
|
+
},
|
|
1750
|
+
received_at: Date.now(),
|
|
1751
|
+
}]);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
catch (e) {
|
|
1755
|
+
console.error(`[handshake] branch extraction failed for task ${taskId}:`, e);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* Reply to pending reverse handshakes when creating a team task.
|
|
1760
|
+
* Uses ack_in (recorded by autoHandshakeRespond) to find task IDs
|
|
1761
|
+
* that the initiator hasn't responded to yet. Bids and submits result
|
|
1762
|
+
* with branch + task details.
|
|
1763
|
+
*/
|
|
1764
|
+
async function replyPendingHandshakes(agentId, team, taskSummary) {
|
|
1765
|
+
for (const [peerId, taskId] of Object.entries(team.ack_in)) {
|
|
1766
|
+
// Bid on the reverse handshake task
|
|
1767
|
+
try {
|
|
1768
|
+
await net.submitBid(taskId, agentId, 0, 1);
|
|
1769
|
+
}
|
|
1770
|
+
catch (e) {
|
|
1771
|
+
console.error(`[handshake] replyPendingHandshakes bid failed for ${agentId} → ${peerId} (task ${taskId}):`, e);
|
|
1772
|
+
continue;
|
|
1773
|
+
}
|
|
1774
|
+
// Submit result with branch + task details
|
|
1775
|
+
// On a 0-budget invited task, bid is typically auto-accepted
|
|
1776
|
+
try {
|
|
1777
|
+
await net.submitResult(taskId, agentId, {
|
|
1778
|
+
_handshake_ack: true,
|
|
1779
|
+
team_id: team.team_id,
|
|
1780
|
+
branch: team.my_branch ?? `agent/${agentId}`,
|
|
1781
|
+
team_task: taskSummary,
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
catch (e) {
|
|
1785
|
+
console.error(`[handshake] replyPendingHandshakes submit failed for ${agentId} → ${peerId} (task ${taskId}):`, e);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
// ---------------------------------------------------------------------------
|
|
1454
1790
|
// Auto-bid evaluation — communication layer auto-filter per agent.md:172-193
|
|
1455
1791
|
// ---------------------------------------------------------------------------
|
|
1456
1792
|
async function autoBidEvaluate(agentId, event) {
|
|
@@ -1481,6 +1817,29 @@ async function autoBidEvaluate(agentId, event) {
|
|
|
1481
1817
|
}]);
|
|
1482
1818
|
}
|
|
1483
1819
|
// ---------------------------------------------------------------------------
|
|
1820
|
+
// Global crash handlers — log to file so post-mortem is possible
|
|
1821
|
+
// ---------------------------------------------------------------------------
|
|
1822
|
+
const __crash_dir = join(dirname(fileURLToPath(import.meta.url)), "..", "logs");
|
|
1823
|
+
function writeCrashLog(label, err) {
|
|
1824
|
+
try {
|
|
1825
|
+
mkdirSync(__crash_dir, { recursive: true });
|
|
1826
|
+
const ts = new Date().toISOString();
|
|
1827
|
+
const msg = err instanceof Error ? `${err.message}\n${err.stack}` : String(err);
|
|
1828
|
+
const line = `[${ts}] ${label}: ${msg}\n`;
|
|
1829
|
+
appendFileSync(join(__crash_dir, "crash.log"), line);
|
|
1830
|
+
console.error(`[EACN3] ${label}:`, msg);
|
|
1831
|
+
}
|
|
1832
|
+
catch { /* last resort — nothing we can do */ }
|
|
1833
|
+
}
|
|
1834
|
+
process.on("uncaughtException", (err) => {
|
|
1835
|
+
writeCrashLog("uncaughtException", err);
|
|
1836
|
+
process.exit(1);
|
|
1837
|
+
});
|
|
1838
|
+
process.on("unhandledRejection", (reason) => {
|
|
1839
|
+
writeCrashLog("unhandledRejection", reason);
|
|
1840
|
+
process.exit(1);
|
|
1841
|
+
});
|
|
1842
|
+
// ---------------------------------------------------------------------------
|
|
1484
1843
|
// Start
|
|
1485
1844
|
// ---------------------------------------------------------------------------
|
|
1486
1845
|
async function main() {
|