multiclaws 0.4.2 → 0.4.3
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/dist/gateway/handlers.js +32 -12
- package/dist/index.js +2 -2
- package/dist/infra/gateway-client.js +10 -0
- package/dist/infra/tailscale.d.ts +1 -1
- package/dist/infra/tailscale.js +14 -5
- package/dist/service/a2a-adapter.js +4 -2
- package/dist/service/agent-registry.js +4 -1
- package/dist/service/multiclaws-service.js +26 -17
- package/dist/service/session-store.js +15 -3
- package/package.json +1 -1
package/dist/gateway/handlers.js
CHANGED
|
@@ -40,9 +40,14 @@ function createGatewayHandlers(getService) {
|
|
|
40
40
|
const handlers = {
|
|
41
41
|
/* ── Agent handlers ─────────────────────────────────────────── */
|
|
42
42
|
"multiclaws.agent.list": async ({ respond }) => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
try {
|
|
44
|
+
const service = getService();
|
|
45
|
+
const agents = await service.listAgents();
|
|
46
|
+
respond(true, { agents });
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
safeHandle(respond, "agent_list_failed", error);
|
|
50
|
+
}
|
|
46
51
|
},
|
|
47
52
|
"multiclaws.agent.add": async ({ params, respond }) => {
|
|
48
53
|
try {
|
|
@@ -187,19 +192,34 @@ function createGatewayHandlers(getService) {
|
|
|
187
192
|
},
|
|
188
193
|
/* ── Profile handlers ───────────────────────────────────────── */
|
|
189
194
|
"multiclaws.profile.show": async ({ respond }) => {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
195
|
+
try {
|
|
196
|
+
const service = getService();
|
|
197
|
+
const profile = await service.getProfile();
|
|
198
|
+
respond(true, profile);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
safeHandle(respond, "profile_show_failed", error);
|
|
202
|
+
}
|
|
193
203
|
},
|
|
194
204
|
"multiclaws.profile.pending_review": async ({ respond }) => {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
205
|
+
try {
|
|
206
|
+
const service = getService();
|
|
207
|
+
const result = await service.getPendingProfileReview();
|
|
208
|
+
respond(true, result);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
safeHandle(respond, "profile_pending_review_failed", error);
|
|
212
|
+
}
|
|
198
213
|
},
|
|
199
214
|
"multiclaws.profile.clear_pending_review": async ({ respond }) => {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
215
|
+
try {
|
|
216
|
+
const service = getService();
|
|
217
|
+
await service.clearPendingProfileReview();
|
|
218
|
+
respond(true, { cleared: true });
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
safeHandle(respond, "profile_clear_pending_review_failed", error);
|
|
222
|
+
}
|
|
203
223
|
},
|
|
204
224
|
"multiclaws.profile.set": async ({ params, respond }) => {
|
|
205
225
|
try {
|
package/dist/index.js
CHANGED
|
@@ -369,7 +369,7 @@ function createTools(getService) {
|
|
|
369
369
|
const plugin = {
|
|
370
370
|
id: "multiclaws",
|
|
371
371
|
name: "MultiClaws",
|
|
372
|
-
version: "0.
|
|
372
|
+
version: "0.4.2",
|
|
373
373
|
register(api) {
|
|
374
374
|
const config = readConfig(api);
|
|
375
375
|
(0, telemetry_1.initializeTelemetry)({ enableConsoleExporter: config.telemetry?.consoleExporter });
|
|
@@ -382,7 +382,7 @@ const plugin = {
|
|
|
382
382
|
if (gw) {
|
|
383
383
|
const tools = (gw.tools ?? {});
|
|
384
384
|
const allow = Array.isArray(tools.allow) ? tools.allow : [];
|
|
385
|
-
const required = ["sessions_spawn", "sessions_history"];
|
|
385
|
+
const required = ["sessions_spawn", "sessions_history", "message"];
|
|
386
386
|
const missing = required.filter((t) => !allow.includes(t));
|
|
387
387
|
if (missing.length > 0) {
|
|
388
388
|
tools.allow = [...allow, ...missing];
|
|
@@ -40,6 +40,7 @@ exports.invokeGatewayTool = invokeGatewayTool;
|
|
|
40
40
|
const opossum_1 = __importDefault(require("opossum"));
|
|
41
41
|
class NonRetryableError extends Error {
|
|
42
42
|
}
|
|
43
|
+
const MAX_BREAKERS = 50;
|
|
43
44
|
const breakerCache = new Map();
|
|
44
45
|
let pRetryModulePromise = null;
|
|
45
46
|
async function loadPRetry() {
|
|
@@ -53,6 +54,15 @@ function getBreaker(key, timeoutMs) {
|
|
|
53
54
|
if (existing) {
|
|
54
55
|
return existing;
|
|
55
56
|
}
|
|
57
|
+
// Evict oldest entries when cache is full
|
|
58
|
+
if (breakerCache.size >= MAX_BREAKERS) {
|
|
59
|
+
const oldest = breakerCache.keys().next().value;
|
|
60
|
+
if (oldest !== undefined) {
|
|
61
|
+
const old = breakerCache.get(oldest);
|
|
62
|
+
old?.shutdown();
|
|
63
|
+
breakerCache.delete(oldest);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
56
66
|
const breaker = new opossum_1.default((operation) => operation(), {
|
|
57
67
|
timeout: false, // timeout handled by AbortController in the operation
|
|
58
68
|
errorThresholdPercentage: 50,
|
|
@@ -10,7 +10,7 @@ export type TailscaleStatus = {
|
|
|
10
10
|
status: "unavailable";
|
|
11
11
|
reason: string;
|
|
12
12
|
};
|
|
13
|
-
/** Check network interfaces for a Tailscale IP (100.
|
|
13
|
+
/** Check network interfaces for a Tailscale IP (100.64.0.0/10) — exported for fast-path checks */
|
|
14
14
|
export declare function getTailscaleIpFromInterfaces(): string | null;
|
|
15
15
|
/**
|
|
16
16
|
* Detect Tailscale status — does NOT install or modify system state.
|
package/dist/infra/tailscale.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.getTailscaleIpFromInterfaces = getTailscaleIpFromInterfaces;
|
|
|
7
7
|
exports.detectTailscale = detectTailscale;
|
|
8
8
|
const node_child_process_1 = require("node:child_process");
|
|
9
9
|
const node_os_1 = __importDefault(require("node:os"));
|
|
10
|
+
const isWindows = process.platform === "win32";
|
|
10
11
|
function run(cmd, timeoutMs = 5_000) {
|
|
11
12
|
return (0, node_child_process_1.execSync)(cmd, { timeout: timeoutMs, stdio: ["ignore", "pipe", "pipe"] })
|
|
12
13
|
.toString()
|
|
@@ -14,21 +15,30 @@ function run(cmd, timeoutMs = 5_000) {
|
|
|
14
15
|
}
|
|
15
16
|
function commandExists(cmd) {
|
|
16
17
|
try {
|
|
17
|
-
run(`which ${cmd}`);
|
|
18
|
+
run(isWindows ? `where ${cmd}` : `which ${cmd}`);
|
|
18
19
|
return true;
|
|
19
20
|
}
|
|
20
21
|
catch {
|
|
21
22
|
return false;
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
|
-
/** Check
|
|
25
|
+
/** Check whether an IPv4 address falls within the Tailscale CGNAT range (100.64.0.0/10). */
|
|
26
|
+
function isTailscaleCGNAT(ip) {
|
|
27
|
+
const parts = ip.split(".");
|
|
28
|
+
if (parts.length !== 4)
|
|
29
|
+
return false;
|
|
30
|
+
const first = parseInt(parts[0], 10);
|
|
31
|
+
const second = parseInt(parts[1], 10);
|
|
32
|
+
return first === 100 && second >= 64 && second <= 127;
|
|
33
|
+
}
|
|
34
|
+
/** Check network interfaces for a Tailscale IP (100.64.0.0/10) — exported for fast-path checks */
|
|
25
35
|
function getTailscaleIpFromInterfaces() {
|
|
26
36
|
const interfaces = node_os_1.default.networkInterfaces();
|
|
27
37
|
for (const addrs of Object.values(interfaces)) {
|
|
28
38
|
if (!addrs)
|
|
29
39
|
continue;
|
|
30
40
|
for (const addr of addrs) {
|
|
31
|
-
if (addr.family === "IPv4" && addr.address
|
|
41
|
+
if (addr.family === "IPv4" && isTailscaleCGNAT(addr.address)) {
|
|
32
42
|
return addr.address;
|
|
33
43
|
}
|
|
34
44
|
}
|
|
@@ -61,8 +71,7 @@ async function getAuthUrl() {
|
|
|
61
71
|
return new Promise((resolve) => {
|
|
62
72
|
try {
|
|
63
73
|
// tailscale up prints the auth URL to stderr
|
|
64
|
-
const
|
|
65
|
-
const proc = spawn("tailscale", ["up"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
74
|
+
const proc = (0, node_child_process_1.spawn)("tailscale", ["up"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
66
75
|
let output = "";
|
|
67
76
|
let resolved = false;
|
|
68
77
|
const tryResolve = (text) => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OpenClawAgentExecutor = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
4
5
|
const gateway_client_1 = require("../infra/gateway-client");
|
|
5
6
|
function extractTextFromMessage(message) {
|
|
6
7
|
if (!message.parts)
|
|
@@ -23,7 +24,7 @@ function buildTaskWithHistory(context) {
|
|
|
23
24
|
.slice(-8) // keep last 8 messages max to avoid huge prompts
|
|
24
25
|
.map((m) => {
|
|
25
26
|
const text = extractTextFromMessage(m);
|
|
26
|
-
const role = m.role === "agent" ? "
|
|
27
|
+
const role = m.role === "agent" ? "agent" : "user";
|
|
27
28
|
return `[${role}]: ${text}`;
|
|
28
29
|
})
|
|
29
30
|
.filter((line) => line.length > 10)
|
|
@@ -96,6 +97,7 @@ class OpenClawAgentExecutor {
|
|
|
96
97
|
if (!this.gatewayConfig) {
|
|
97
98
|
this.logger.error("[a2a-adapter] gateway config not available, cannot execute task");
|
|
98
99
|
this.taskTracker.update(trackedId, { status: "failed", error: "gateway config not available" });
|
|
100
|
+
this.a2aToTracker.delete(taskId);
|
|
99
101
|
this.publishMessage(eventBus, "Error: gateway config not available, cannot execute task.");
|
|
100
102
|
eventBus.finished();
|
|
101
103
|
return;
|
|
@@ -243,7 +245,7 @@ class OpenClawAgentExecutor {
|
|
|
243
245
|
const message = {
|
|
244
246
|
kind: "message",
|
|
245
247
|
role: "agent",
|
|
246
|
-
messageId:
|
|
248
|
+
messageId: (0, node_crypto_1.randomUUID)(),
|
|
247
249
|
parts: [{ kind: "text", text }],
|
|
248
250
|
};
|
|
249
251
|
eventBus.publish(message);
|
|
@@ -32,14 +32,17 @@ class AgentRegistry {
|
|
|
32
32
|
const normalizedUrl = params.url.replace(/\/+$/, "");
|
|
33
33
|
const existing = store.agents.findIndex((a) => a.url === normalizedUrl);
|
|
34
34
|
const now = Date.now();
|
|
35
|
+
const prev = existing >= 0 ? store.agents[existing] : null;
|
|
35
36
|
const record = {
|
|
36
37
|
url: normalizedUrl,
|
|
37
38
|
name: params.name,
|
|
38
39
|
description: params.description ?? "",
|
|
39
40
|
skills: params.skills ?? [],
|
|
40
41
|
apiKey: params.apiKey,
|
|
41
|
-
addedAtMs:
|
|
42
|
+
addedAtMs: prev?.addedAtMs ?? now,
|
|
42
43
|
lastSeenAtMs: now,
|
|
44
|
+
// Preserve existing teamIds so that team associations are not lost on upsert
|
|
45
|
+
...(prev?.teamIds?.length ? { teamIds: prev.teamIds } : {}),
|
|
43
46
|
};
|
|
44
47
|
if (existing >= 0) {
|
|
45
48
|
store.agents[existing] = record;
|
|
@@ -110,7 +110,7 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
110
110
|
name: this.options.displayName ?? (profile.ownerName || "OpenClaw Agent"),
|
|
111
111
|
description: this.profileDescription,
|
|
112
112
|
url: this.selfUrl,
|
|
113
|
-
version: "0.
|
|
113
|
+
version: "0.4.2",
|
|
114
114
|
protocolVersion: "0.2.2",
|
|
115
115
|
defaultInputModes: ["text/plain"],
|
|
116
116
|
defaultOutputModes: ["text/plain"],
|
|
@@ -149,7 +149,13 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
149
149
|
}));
|
|
150
150
|
const listenPort = this.options.port ?? 3100;
|
|
151
151
|
this.httpServer = node_http_1.default.createServer(app);
|
|
152
|
-
await new Promise((resolve) =>
|
|
152
|
+
await new Promise((resolve, reject) => {
|
|
153
|
+
this.httpServer.once("error", reject);
|
|
154
|
+
this.httpServer.listen(listenPort, "0.0.0.0", () => {
|
|
155
|
+
this.httpServer.removeListener("error", reject);
|
|
156
|
+
resolve();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
153
159
|
this.started = true;
|
|
154
160
|
this.log("info", `multiclaws A2A service listening on :${listenPort}`);
|
|
155
161
|
}
|
|
@@ -351,18 +357,20 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
351
357
|
const timer = setTimeout(() => abortController.abort(), timeout);
|
|
352
358
|
try {
|
|
353
359
|
const client = await this.createA2AClient(params.agentRecord);
|
|
354
|
-
const withAbort = (p) =>
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
360
|
+
const withAbort = (p) => {
|
|
361
|
+
if (abortController.signal.aborted) {
|
|
362
|
+
return Promise.reject(new Error("session canceled"));
|
|
363
|
+
}
|
|
364
|
+
return new Promise((resolve, reject) => {
|
|
365
|
+
const onAbort = () => {
|
|
366
|
+
reject(new Error(this.sessionStore.get(params.sessionId)?.status === "canceled"
|
|
367
|
+
? "session canceled"
|
|
368
|
+
: "session timeout"));
|
|
369
|
+
};
|
|
370
|
+
abortController.signal.addEventListener("abort", onAbort, { once: true });
|
|
371
|
+
p.then((val) => { abortController.signal.removeEventListener("abort", onAbort); resolve(val); }, (err) => { abortController.signal.removeEventListener("abort", onAbort); reject(err); });
|
|
372
|
+
});
|
|
373
|
+
};
|
|
366
374
|
let result = await withAbort(client.sendMessage({
|
|
367
375
|
message: {
|
|
368
376
|
kind: "message",
|
|
@@ -814,7 +822,7 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
814
822
|
}
|
|
815
823
|
const normalizedUrl = parsed.data.url.replace(/\/+$/, "");
|
|
816
824
|
await this.teamStore.removeMember(team.teamId, normalizedUrl);
|
|
817
|
-
await this.agentRegistry.
|
|
825
|
+
await this.agentRegistry.removeTeamSource(normalizedUrl, team.teamId);
|
|
818
826
|
res.json({ ok: true });
|
|
819
827
|
}
|
|
820
828
|
catch (err) {
|
|
@@ -864,12 +872,13 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
864
872
|
const selfNormalized = this.selfUrl.replace(/\/+$/, "");
|
|
865
873
|
const displayName = this.options.displayName ?? node_os_1.default.hostname();
|
|
866
874
|
for (const team of teams) {
|
|
867
|
-
// Update self in team store
|
|
875
|
+
// Update self in team store, preserving original joinedAtMs
|
|
876
|
+
const selfMember = team.members.find((m) => m.url.replace(/\/+$/, "") === selfNormalized);
|
|
868
877
|
await this.teamStore.addMember(team.teamId, {
|
|
869
878
|
url: this.selfUrl,
|
|
870
879
|
name: displayName,
|
|
871
880
|
description: this.profileDescription,
|
|
872
|
-
joinedAtMs: Date.now(),
|
|
881
|
+
joinedAtMs: selfMember?.joinedAtMs ?? Date.now(),
|
|
873
882
|
});
|
|
874
883
|
// Broadcast to other members
|
|
875
884
|
const others = team.members.filter((m) => m.url.replace(/\/+$/, "") !== selfNormalized);
|
|
@@ -14,6 +14,20 @@ const MAX_MESSAGES_PER_SESSION = 200;
|
|
|
14
14
|
function emptyStore() {
|
|
15
15
|
return { version: 1, sessions: [] };
|
|
16
16
|
}
|
|
17
|
+
function normalizeStore(raw) {
|
|
18
|
+
if (raw.version !== 1 || !Array.isArray(raw.sessions)) {
|
|
19
|
+
return emptyStore();
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
version: 1,
|
|
23
|
+
sessions: raw.sessions.filter((s) => s &&
|
|
24
|
+
typeof s.sessionId === "string" &&
|
|
25
|
+
typeof s.agentUrl === "string" &&
|
|
26
|
+
typeof s.status === "string" &&
|
|
27
|
+
typeof s.createdAtMs === "number" &&
|
|
28
|
+
Array.isArray(s.messages)),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
17
31
|
class SessionStore {
|
|
18
32
|
filePath;
|
|
19
33
|
ttlMs;
|
|
@@ -78,9 +92,7 @@ class SessionStore {
|
|
|
78
92
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(this.filePath), { recursive: true });
|
|
79
93
|
try {
|
|
80
94
|
const raw = JSON.parse(node_fs_1.default.readFileSync(this.filePath, "utf8"));
|
|
81
|
-
|
|
82
|
-
return emptyStore();
|
|
83
|
-
return raw;
|
|
95
|
+
return normalizeStore(raw);
|
|
84
96
|
}
|
|
85
97
|
catch {
|
|
86
98
|
const store = emptyStore();
|