@perkos/perkos-a2a 0.12.30 → 0.12.32
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/README.md +27 -9
- package/dist/chat-client.d.ts +6 -0
- package/dist/chat-client.d.ts.map +1 -1
- package/dist/chat-client.js +80 -22
- package/dist/chat-client.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +957 -435
- package/dist/index.js.map +4 -4
- package/dist/openclaw-supervisor-cli.d.ts +3 -0
- package/dist/openclaw-supervisor-cli.d.ts.map +1 -0
- package/dist/openclaw-supervisor-cli.js +18 -0
- package/dist/openclaw-supervisor-cli.js.map +1 -0
- package/dist/openclaw-supervisor-client.d.ts +68 -0
- package/dist/openclaw-supervisor-client.d.ts.map +1 -0
- package/dist/openclaw-supervisor-client.js +301 -0
- package/dist/openclaw-supervisor-client.js.map +1 -0
- package/dist/openclaw-supervisor.d.ts +49 -0
- package/dist/openclaw-supervisor.d.ts.map +1 -0
- package/dist/openclaw-supervisor.js +443 -0
- package/dist/openclaw-supervisor.js.map +1 -0
- package/dist/platform-heartbeat.d.ts +5 -0
- package/dist/platform-heartbeat.d.ts.map +1 -1
- package/dist/platform-heartbeat.js +3 -0
- package/dist/platform-heartbeat.js.map +1 -1
- package/dist/relay-client.d.ts +7 -0
- package/dist/relay-client.d.ts.map +1 -1
- package/dist/relay-client.js +89 -18
- package/dist/relay-client.js.map +1 -1
- package/dist/relay.d.ts +1 -1
- package/dist/relay.d.ts.map +1 -1
- package/dist/relay.js +21 -3
- package/dist/relay.js.map +1 -1
- package/dist/repair.d.ts +10 -3
- package/dist/repair.d.ts.map +1 -1
- package/dist/repair.js +118 -45
- package/dist/repair.js.map +1 -1
- package/dist/runtime-evidence.d.ts +1 -0
- package/dist/runtime-evidence.d.ts.map +1 -1
- package/dist/runtime-evidence.js +3 -0
- package/dist/runtime-evidence.js.map +1 -1
- package/dist/server.d.ts +33 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +200 -18
- package/dist/server.js.map +1 -1
- package/dist/supervisor-protocol.d.ts +123 -0
- package/dist/supervisor-protocol.d.ts.map +1 -0
- package/dist/supervisor-protocol.js +57 -0
- package/dist/supervisor-protocol.js.map +1 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1297,8 +1297,8 @@ var require_node = __commonJS({
|
|
|
1297
1297
|
break;
|
|
1298
1298
|
case "PIPE":
|
|
1299
1299
|
case "TCP":
|
|
1300
|
-
var
|
|
1301
|
-
stream2 = new
|
|
1300
|
+
var net2 = __require("net");
|
|
1301
|
+
stream2 = new net2.Socket({
|
|
1302
1302
|
fd: fd2,
|
|
1303
1303
|
readable: false,
|
|
1304
1304
|
writable: true
|
|
@@ -4944,10 +4944,10 @@ var require_raw_body = __commonJS({
|
|
|
4944
4944
|
if (done) {
|
|
4945
4945
|
return readStream(stream, encoding, length, limit, wrap(done));
|
|
4946
4946
|
}
|
|
4947
|
-
return new Promise(function executor(
|
|
4947
|
+
return new Promise(function executor(resolve4, reject) {
|
|
4948
4948
|
readStream(stream, encoding, length, limit, function onRead(err, buf) {
|
|
4949
4949
|
if (err) return reject(err);
|
|
4950
|
-
|
|
4950
|
+
resolve4(buf);
|
|
4951
4951
|
});
|
|
4952
4952
|
});
|
|
4953
4953
|
}
|
|
@@ -18429,11 +18429,11 @@ var require_view = __commonJS({
|
|
|
18429
18429
|
var debug = require_src()("express:view");
|
|
18430
18430
|
var path = __require("path");
|
|
18431
18431
|
var fs = __require("fs");
|
|
18432
|
-
var
|
|
18432
|
+
var dirname3 = path.dirname;
|
|
18433
18433
|
var basename = path.basename;
|
|
18434
18434
|
var extname = path.extname;
|
|
18435
|
-
var
|
|
18436
|
-
var
|
|
18435
|
+
var join4 = path.join;
|
|
18436
|
+
var resolve4 = path.resolve;
|
|
18437
18437
|
module.exports = View;
|
|
18438
18438
|
function View(name, options) {
|
|
18439
18439
|
var opts = options || {};
|
|
@@ -18467,8 +18467,8 @@ var require_view = __commonJS({
|
|
|
18467
18467
|
debug('lookup "%s"', name);
|
|
18468
18468
|
for (var i = 0; i < roots.length && !path2; i++) {
|
|
18469
18469
|
var root = roots[i];
|
|
18470
|
-
var loc =
|
|
18471
|
-
var dir =
|
|
18470
|
+
var loc = resolve4(root, name);
|
|
18471
|
+
var dir = dirname3(loc);
|
|
18472
18472
|
var file = basename(loc);
|
|
18473
18473
|
path2 = this.resolve(dir, file);
|
|
18474
18474
|
}
|
|
@@ -18478,14 +18478,14 @@ var require_view = __commonJS({
|
|
|
18478
18478
|
debug('render "%s"', this.path);
|
|
18479
18479
|
this.engine(this.path, options, callback);
|
|
18480
18480
|
};
|
|
18481
|
-
View.prototype.resolve = function
|
|
18481
|
+
View.prototype.resolve = function resolve5(dir, file) {
|
|
18482
18482
|
var ext = this.ext;
|
|
18483
|
-
var path2 =
|
|
18483
|
+
var path2 = join4(dir, file);
|
|
18484
18484
|
var stat = tryStat(path2);
|
|
18485
18485
|
if (stat && stat.isFile()) {
|
|
18486
18486
|
return path2;
|
|
18487
18487
|
}
|
|
18488
|
-
path2 =
|
|
18488
|
+
path2 = join4(dir, basename(file, ext), "index" + ext);
|
|
18489
18489
|
stat = tryStat(path2);
|
|
18490
18490
|
if (stat && stat.isFile()) {
|
|
18491
18491
|
return path2;
|
|
@@ -19118,9 +19118,9 @@ var require_send = __commonJS({
|
|
|
19118
19118
|
var Stream = __require("stream");
|
|
19119
19119
|
var util = __require("util");
|
|
19120
19120
|
var extname = path.extname;
|
|
19121
|
-
var
|
|
19121
|
+
var join4 = path.join;
|
|
19122
19122
|
var normalize = path.normalize;
|
|
19123
|
-
var
|
|
19123
|
+
var resolve4 = path.resolve;
|
|
19124
19124
|
var sep = path.sep;
|
|
19125
19125
|
var BYTES_RANGE_REGEXP = /^ *bytes=/;
|
|
19126
19126
|
var MAX_MAXAGE = 60 * 60 * 24 * 365 * 1e3;
|
|
@@ -19157,7 +19157,7 @@ var require_send = __commonJS({
|
|
|
19157
19157
|
this._maxage = opts.maxAge || opts.maxage;
|
|
19158
19158
|
this._maxage = typeof this._maxage === "string" ? ms(this._maxage) : Number(this._maxage);
|
|
19159
19159
|
this._maxage = !isNaN(this._maxage) ? Math.min(Math.max(0, this._maxage), MAX_MAXAGE) : 0;
|
|
19160
|
-
this._root = opts.root ?
|
|
19160
|
+
this._root = opts.root ? resolve4(opts.root) : null;
|
|
19161
19161
|
if (!this._root && opts.from) {
|
|
19162
19162
|
this.from(opts.from);
|
|
19163
19163
|
}
|
|
@@ -19181,7 +19181,7 @@ var require_send = __commonJS({
|
|
|
19181
19181
|
return this;
|
|
19182
19182
|
}, "send.index: pass index as option");
|
|
19183
19183
|
SendStream.prototype.root = function root(path2) {
|
|
19184
|
-
this._root =
|
|
19184
|
+
this._root = resolve4(String(path2));
|
|
19185
19185
|
debug("root %s", this._root);
|
|
19186
19186
|
return this;
|
|
19187
19187
|
};
|
|
@@ -19337,7 +19337,7 @@ var require_send = __commonJS({
|
|
|
19337
19337
|
return res;
|
|
19338
19338
|
}
|
|
19339
19339
|
parts = path2.split(sep);
|
|
19340
|
-
path2 = normalize(
|
|
19340
|
+
path2 = normalize(join4(root, path2));
|
|
19341
19341
|
} else {
|
|
19342
19342
|
if (UP_PATH_REGEXP.test(path2)) {
|
|
19343
19343
|
debug('malicious path "%s"', path2);
|
|
@@ -19345,7 +19345,7 @@ var require_send = __commonJS({
|
|
|
19345
19345
|
return res;
|
|
19346
19346
|
}
|
|
19347
19347
|
parts = normalize(path2).split(sep);
|
|
19348
|
-
path2 =
|
|
19348
|
+
path2 = resolve4(path2);
|
|
19349
19349
|
}
|
|
19350
19350
|
if (containsDotFile(parts)) {
|
|
19351
19351
|
var access = this._dotfiles;
|
|
@@ -19472,7 +19472,7 @@ var require_send = __commonJS({
|
|
|
19472
19472
|
if (err) return self.onStatError(err);
|
|
19473
19473
|
return self.error(404);
|
|
19474
19474
|
}
|
|
19475
|
-
var p =
|
|
19475
|
+
var p = join4(path2, self._index[i]);
|
|
19476
19476
|
debug('stat "%s"', p);
|
|
19477
19477
|
fs.stat(p, function(err2, stat) {
|
|
19478
19478
|
if (err2) return next(err2);
|
|
@@ -20625,7 +20625,7 @@ var require_application = __commonJS({
|
|
|
20625
20625
|
var deprecate = require_depd()("express");
|
|
20626
20626
|
var flatten = require_array_flatten();
|
|
20627
20627
|
var merge = require_utils_merge();
|
|
20628
|
-
var
|
|
20628
|
+
var resolve4 = __require("path").resolve;
|
|
20629
20629
|
var setPrototypeOf = require_setprototypeof();
|
|
20630
20630
|
var hasOwnProperty = Object.prototype.hasOwnProperty;
|
|
20631
20631
|
var slice = Array.prototype.slice;
|
|
@@ -20664,7 +20664,7 @@ var require_application = __commonJS({
|
|
|
20664
20664
|
this.mountpath = "/";
|
|
20665
20665
|
this.locals.settings = this.settings;
|
|
20666
20666
|
this.set("view", View);
|
|
20667
|
-
this.set("views",
|
|
20667
|
+
this.set("views", resolve4("views"));
|
|
20668
20668
|
this.set("jsonp callback name", "callback");
|
|
20669
20669
|
if (env === "production") {
|
|
20670
20670
|
this.enable("view cache");
|
|
@@ -21909,7 +21909,7 @@ var require_response = __commonJS({
|
|
|
21909
21909
|
var send = require_send();
|
|
21910
21910
|
var extname = path.extname;
|
|
21911
21911
|
var mime = send.mime;
|
|
21912
|
-
var
|
|
21912
|
+
var resolve4 = path.resolve;
|
|
21913
21913
|
var vary = require_vary();
|
|
21914
21914
|
var res = Object.create(http.ServerResponse.prototype);
|
|
21915
21915
|
module.exports = res;
|
|
@@ -22168,7 +22168,7 @@ var require_response = __commonJS({
|
|
|
22168
22168
|
}
|
|
22169
22169
|
opts = Object.create(opts);
|
|
22170
22170
|
opts.headers = headers;
|
|
22171
|
-
var fullPath = !opts.root ?
|
|
22171
|
+
var fullPath = !opts.root ? resolve4(path2) : path2;
|
|
22172
22172
|
return this.sendFile(fullPath, opts, done);
|
|
22173
22173
|
};
|
|
22174
22174
|
res.contentType = res.type = function contentType(type) {
|
|
@@ -22434,7 +22434,7 @@ var require_serve_static = __commonJS({
|
|
|
22434
22434
|
var encodeUrl = require_encodeurl();
|
|
22435
22435
|
var escapeHtml = require_escape_html();
|
|
22436
22436
|
var parseUrl = require_parseurl();
|
|
22437
|
-
var
|
|
22437
|
+
var resolve4 = __require("path").resolve;
|
|
22438
22438
|
var send = require_send();
|
|
22439
22439
|
var url = __require("url");
|
|
22440
22440
|
module.exports = serveStatic;
|
|
@@ -22454,7 +22454,7 @@ var require_serve_static = __commonJS({
|
|
|
22454
22454
|
throw new TypeError("option setHeaders must be function");
|
|
22455
22455
|
}
|
|
22456
22456
|
opts.maxage = opts.maxage || opts.maxAge || 0;
|
|
22457
|
-
opts.root =
|
|
22457
|
+
opts.root = resolve4(root);
|
|
22458
22458
|
var onDirectory = redirect ? createRedirectDirectoryListener() : createNotFoundDirectoryListener();
|
|
22459
22459
|
return function serveStatic2(req, res, next) {
|
|
22460
22460
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
@@ -24815,9 +24815,9 @@ var require_websocket = __commonJS({
|
|
|
24815
24815
|
var EventEmitter = __require("events");
|
|
24816
24816
|
var https = __require("https");
|
|
24817
24817
|
var http = __require("http");
|
|
24818
|
-
var
|
|
24818
|
+
var net2 = __require("net");
|
|
24819
24819
|
var tls = __require("tls");
|
|
24820
|
-
var { randomBytes, createHash:
|
|
24820
|
+
var { randomBytes, createHash: createHash3 } = __require("crypto");
|
|
24821
24821
|
var { Duplex, Readable } = __require("stream");
|
|
24822
24822
|
var { URL: URL2 } = __require("url");
|
|
24823
24823
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -25477,7 +25477,7 @@ var require_websocket = __commonJS({
|
|
|
25477
25477
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
25478
25478
|
return;
|
|
25479
25479
|
}
|
|
25480
|
-
const digest =
|
|
25480
|
+
const digest = createHash3("sha1").update(key + GUID).digest("base64");
|
|
25481
25481
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
25482
25482
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
25483
25483
|
return;
|
|
@@ -25549,12 +25549,12 @@ var require_websocket = __commonJS({
|
|
|
25549
25549
|
}
|
|
25550
25550
|
function netConnect(options) {
|
|
25551
25551
|
options.path = options.socketPath;
|
|
25552
|
-
return
|
|
25552
|
+
return net2.connect(options);
|
|
25553
25553
|
}
|
|
25554
25554
|
function tlsConnect(options) {
|
|
25555
25555
|
options.path = void 0;
|
|
25556
25556
|
if (!options.servername && options.servername !== "") {
|
|
25557
|
-
options.servername =
|
|
25557
|
+
options.servername = net2.isIP(options.host) ? "" : options.host;
|
|
25558
25558
|
}
|
|
25559
25559
|
return tls.connect(options);
|
|
25560
25560
|
}
|
|
@@ -25844,7 +25844,7 @@ var require_websocket_server = __commonJS({
|
|
|
25844
25844
|
var EventEmitter = __require("events");
|
|
25845
25845
|
var http = __require("http");
|
|
25846
25846
|
var { Duplex } = __require("stream");
|
|
25847
|
-
var { createHash:
|
|
25847
|
+
var { createHash: createHash3 } = __require("crypto");
|
|
25848
25848
|
var extension2 = require_extension();
|
|
25849
25849
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
25850
25850
|
var subprotocol2 = require_subprotocol();
|
|
@@ -26145,7 +26145,7 @@ var require_websocket_server = __commonJS({
|
|
|
26145
26145
|
);
|
|
26146
26146
|
}
|
|
26147
26147
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
|
26148
|
-
const digest =
|
|
26148
|
+
const digest = createHash3("sha1").update(key + GUID).digest("base64");
|
|
26149
26149
|
const headers = [
|
|
26150
26150
|
"HTTP/1.1 101 Switching Protocols",
|
|
26151
26151
|
"Upgrade: websocket",
|
|
@@ -26263,6 +26263,7 @@ var RelayClient = class {
|
|
|
26263
26263
|
heartbeatTimer = null;
|
|
26264
26264
|
connected = false;
|
|
26265
26265
|
stopped = false;
|
|
26266
|
+
generation = 0;
|
|
26266
26267
|
pendingCallbacks = /* @__PURE__ */ new Map();
|
|
26267
26268
|
/** Responses that arrived with nobody waiting, claimable by a retry. */
|
|
26268
26269
|
orphanResponses = /* @__PURE__ */ new Map();
|
|
@@ -26274,17 +26275,46 @@ var RelayClient = class {
|
|
|
26274
26275
|
return this.connected;
|
|
26275
26276
|
}
|
|
26276
26277
|
start() {
|
|
26278
|
+
if (!this.stopped && this.ws) return;
|
|
26277
26279
|
this.stopped = false;
|
|
26278
|
-
this.
|
|
26280
|
+
const generation = ++this.generation;
|
|
26281
|
+
this.connect(generation);
|
|
26279
26282
|
}
|
|
26280
26283
|
stop() {
|
|
26281
|
-
|
|
26282
|
-
|
|
26283
|
-
|
|
26284
|
-
|
|
26285
|
-
this.ws = null;
|
|
26284
|
+
const socket = this.beginStop();
|
|
26285
|
+
try {
|
|
26286
|
+
socket?.close(1e3, "Client shutting down");
|
|
26287
|
+
} catch {
|
|
26286
26288
|
}
|
|
26287
|
-
|
|
26289
|
+
}
|
|
26290
|
+
/** Stop reconnect activity and wait a bounded time for the socket to close. */
|
|
26291
|
+
async stopAndWait(timeoutMs = 5e3) {
|
|
26292
|
+
const socket = this.beginStop();
|
|
26293
|
+
if (!socket || socket.readyState === wrapper_default.CLOSED) return { closeTimedOut: false };
|
|
26294
|
+
return new Promise((resolve4) => {
|
|
26295
|
+
let settled = false;
|
|
26296
|
+
const finish = (closeTimedOut) => {
|
|
26297
|
+
if (settled) return;
|
|
26298
|
+
settled = true;
|
|
26299
|
+
clearTimeout(timer);
|
|
26300
|
+
resolve4({ closeTimedOut });
|
|
26301
|
+
};
|
|
26302
|
+
const timer = setTimeout(() => {
|
|
26303
|
+
finish(true);
|
|
26304
|
+
try {
|
|
26305
|
+
socket.terminate();
|
|
26306
|
+
} catch {
|
|
26307
|
+
}
|
|
26308
|
+
}, timeoutMs);
|
|
26309
|
+
timer.unref?.();
|
|
26310
|
+
socket.once("close", () => finish(false));
|
|
26311
|
+
socket.once("error", () => finish(false));
|
|
26312
|
+
try {
|
|
26313
|
+
socket.close(1e3, "Client shutting down");
|
|
26314
|
+
} catch {
|
|
26315
|
+
finish(false);
|
|
26316
|
+
}
|
|
26317
|
+
});
|
|
26288
26318
|
}
|
|
26289
26319
|
/**
|
|
26290
26320
|
* Send a task to another agent via the relay.
|
|
@@ -26333,8 +26363,8 @@ var RelayClient = class {
|
|
|
26333
26363
|
};
|
|
26334
26364
|
this.send(response);
|
|
26335
26365
|
}
|
|
26336
|
-
connect() {
|
|
26337
|
-
if (this.
|
|
26366
|
+
connect(generation) {
|
|
26367
|
+
if (!this.isCurrent(generation)) return;
|
|
26338
26368
|
const url = this.options.relay.url;
|
|
26339
26369
|
this.logger.info(`[perkos-a2a] Connecting to relay: ${url}`);
|
|
26340
26370
|
try {
|
|
@@ -26342,17 +26372,23 @@ var RelayClient = class {
|
|
|
26342
26372
|
} catch (err) {
|
|
26343
26373
|
const msg = err instanceof Error ? err.message : String(err);
|
|
26344
26374
|
this.logger.error(`[perkos-a2a] Failed to create WebSocket: ${msg}`);
|
|
26345
|
-
this.scheduleReconnect();
|
|
26375
|
+
this.scheduleReconnect(generation);
|
|
26346
26376
|
return;
|
|
26347
26377
|
}
|
|
26348
|
-
this.ws
|
|
26378
|
+
const socket = this.ws;
|
|
26379
|
+
socket.on("open", () => {
|
|
26380
|
+
if (!this.isCurrent(generation, socket)) {
|
|
26381
|
+
socket.close(1e3, "Stale relay generation");
|
|
26382
|
+
return;
|
|
26383
|
+
}
|
|
26349
26384
|
this.connected = true;
|
|
26350
26385
|
this.reconnectMs = MIN_RECONNECT_MS;
|
|
26351
26386
|
this.logger.info("[perkos-a2a] Connected to relay hub");
|
|
26352
26387
|
this.register();
|
|
26353
26388
|
this.startHeartbeat();
|
|
26354
26389
|
});
|
|
26355
|
-
|
|
26390
|
+
socket.on("message", (data) => {
|
|
26391
|
+
if (!this.isCurrent(generation, socket)) return;
|
|
26356
26392
|
let msg;
|
|
26357
26393
|
try {
|
|
26358
26394
|
msg = JSON.parse(data.toString());
|
|
@@ -26362,15 +26398,17 @@ var RelayClient = class {
|
|
|
26362
26398
|
}
|
|
26363
26399
|
this.handleMessage(msg);
|
|
26364
26400
|
});
|
|
26365
|
-
|
|
26401
|
+
socket.on("close", (code, reason) => {
|
|
26402
|
+
if (!this.isCurrent(generation, socket)) return;
|
|
26366
26403
|
this.connected = false;
|
|
26367
26404
|
this.clearTimers();
|
|
26368
26405
|
if (!this.stopped) {
|
|
26369
26406
|
this.logger.info(`[perkos-a2a] Relay connection closed (${code}: ${reason || "no reason"}). Reconnecting...`);
|
|
26370
|
-
this.scheduleReconnect();
|
|
26407
|
+
this.scheduleReconnect(generation);
|
|
26371
26408
|
}
|
|
26372
26409
|
});
|
|
26373
|
-
|
|
26410
|
+
socket.on("error", (err) => {
|
|
26411
|
+
if (!this.isCurrent(generation, socket)) return;
|
|
26374
26412
|
this.logger.error(`[perkos-a2a] Relay WebSocket error: ${err.message}`);
|
|
26375
26413
|
});
|
|
26376
26414
|
}
|
|
@@ -26450,7 +26488,7 @@ var RelayClient = class {
|
|
|
26450
26488
|
}
|
|
26451
26489
|
}
|
|
26452
26490
|
async sendAndWait(msg, expectedType, timeoutMs) {
|
|
26453
|
-
return new Promise((
|
|
26491
|
+
return new Promise((resolve4, reject) => {
|
|
26454
26492
|
const timer = setTimeout(() => {
|
|
26455
26493
|
this.pendingCallbacks.delete(msg.id);
|
|
26456
26494
|
reject(new Error(`Relay request timed out after ${timeoutMs}ms`));
|
|
@@ -26462,8 +26500,12 @@ var RelayClient = class {
|
|
|
26462
26500
|
if (response.type === "error") {
|
|
26463
26501
|
reject(new Error(`Relay error: ${response.payload.message}`));
|
|
26464
26502
|
} else {
|
|
26465
|
-
|
|
26503
|
+
resolve4(response);
|
|
26466
26504
|
}
|
|
26505
|
+
},
|
|
26506
|
+
cancel: (error) => {
|
|
26507
|
+
clearTimeout(timer);
|
|
26508
|
+
reject(error);
|
|
26467
26509
|
}
|
|
26468
26510
|
});
|
|
26469
26511
|
this.send(msg);
|
|
@@ -26481,10 +26523,10 @@ var RelayClient = class {
|
|
|
26481
26523
|
this.send(msg);
|
|
26482
26524
|
}, HEARTBEAT_INTERVAL_MS);
|
|
26483
26525
|
}
|
|
26484
|
-
scheduleReconnect() {
|
|
26485
|
-
if (this.
|
|
26526
|
+
scheduleReconnect(generation) {
|
|
26527
|
+
if (!this.isCurrent(generation)) return;
|
|
26486
26528
|
this.reconnectTimer = setTimeout(() => {
|
|
26487
|
-
this.connect();
|
|
26529
|
+
if (this.isCurrent(generation)) this.connect(generation);
|
|
26488
26530
|
}, this.reconnectMs);
|
|
26489
26531
|
this.reconnectMs = Math.min(this.reconnectMs * 2, MAX_RECONNECT_MS);
|
|
26490
26532
|
}
|
|
@@ -26498,6 +26540,23 @@ var RelayClient = class {
|
|
|
26498
26540
|
this.reconnectTimer = null;
|
|
26499
26541
|
}
|
|
26500
26542
|
}
|
|
26543
|
+
isCurrent(generation, socket) {
|
|
26544
|
+
return !this.stopped && generation === this.generation && (!socket || socket === this.ws);
|
|
26545
|
+
}
|
|
26546
|
+
beginStop() {
|
|
26547
|
+
if (this.stopped && !this.ws) return null;
|
|
26548
|
+
this.stopped = true;
|
|
26549
|
+
this.generation += 1;
|
|
26550
|
+
this.clearTimers();
|
|
26551
|
+
const error = new Error("CLIENT_STOPPED");
|
|
26552
|
+
for (const pending of this.pendingCallbacks.values()) pending.cancel(error);
|
|
26553
|
+
this.pendingCallbacks.clear();
|
|
26554
|
+
this.orphanResponses.clear();
|
|
26555
|
+
const socket = this.ws;
|
|
26556
|
+
this.ws = null;
|
|
26557
|
+
this.connected = false;
|
|
26558
|
+
return socket;
|
|
26559
|
+
}
|
|
26501
26560
|
};
|
|
26502
26561
|
|
|
26503
26562
|
// src/relay.ts
|
|
@@ -26533,17 +26592,33 @@ var RelayHub = class {
|
|
|
26533
26592
|
});
|
|
26534
26593
|
this.heartbeatTimer = setInterval(() => this.checkHeartbeats(), this.config.heartbeatIntervalMs);
|
|
26535
26594
|
}
|
|
26536
|
-
stop() {
|
|
26595
|
+
async stop(timeoutMs = 5e3) {
|
|
26537
26596
|
if (this.heartbeatTimer) {
|
|
26538
26597
|
clearInterval(this.heartbeatTimer);
|
|
26539
26598
|
this.heartbeatTimer = null;
|
|
26540
26599
|
}
|
|
26541
26600
|
if (this.wss) {
|
|
26601
|
+
const wss = this.wss;
|
|
26602
|
+
this.wss = null;
|
|
26542
26603
|
for (const agent of this.agents.values()) {
|
|
26543
26604
|
agent.ws.close(1001, "Relay shutting down");
|
|
26544
26605
|
}
|
|
26545
|
-
|
|
26546
|
-
|
|
26606
|
+
await new Promise((resolve4) => {
|
|
26607
|
+
let settled = false;
|
|
26608
|
+
const done = () => {
|
|
26609
|
+
if (settled) return;
|
|
26610
|
+
settled = true;
|
|
26611
|
+
clearTimeout(timer);
|
|
26612
|
+
resolve4();
|
|
26613
|
+
};
|
|
26614
|
+
const timer = setTimeout(() => {
|
|
26615
|
+
for (const client of wss.clients) client.terminate();
|
|
26616
|
+
done();
|
|
26617
|
+
}, timeoutMs);
|
|
26618
|
+
timer.unref?.();
|
|
26619
|
+
wss.close(() => done());
|
|
26620
|
+
});
|
|
26621
|
+
this.agents.clear();
|
|
26547
26622
|
this.logger.info("[perkos-a2a] Relay hub stopped");
|
|
26548
26623
|
}
|
|
26549
26624
|
}
|
|
@@ -26933,11 +27008,18 @@ var A2AServer = class {
|
|
|
26933
27008
|
logger;
|
|
26934
27009
|
clientOnly = false;
|
|
26935
27010
|
relayClient = null;
|
|
27011
|
+
externalRelayTransport = false;
|
|
26936
27012
|
relayHub = null;
|
|
26937
27013
|
messageInjector = null;
|
|
26938
27014
|
taskResultHandler = null;
|
|
26939
27015
|
taskFailureHandler = null;
|
|
26940
27016
|
lastRuntimeDelivery = null;
|
|
27017
|
+
httpServer = null;
|
|
27018
|
+
lifecycleState = "new";
|
|
27019
|
+
generation = 0;
|
|
27020
|
+
startPromise = null;
|
|
27021
|
+
stopPromise = null;
|
|
27022
|
+
operations = /* @__PURE__ */ new Set();
|
|
26941
27023
|
constructor(config, logger) {
|
|
26942
27024
|
this.config = config;
|
|
26943
27025
|
this.logger = logger || { info: console.log, error: console.error };
|
|
@@ -26966,6 +27048,14 @@ var A2AServer = class {
|
|
|
26966
27048
|
setTaskFailureHandler(handler) {
|
|
26967
27049
|
this.taskFailureHandler = handler;
|
|
26968
27050
|
}
|
|
27051
|
+
/** Use a process-external owner for relay transport (OpenClaw supervisor). */
|
|
27052
|
+
setRelayTransport(transport) {
|
|
27053
|
+
if (this.lifecycleState !== "new" && this.lifecycleState !== "stopped") {
|
|
27054
|
+
throw new Error("relay transport must be configured before start");
|
|
27055
|
+
}
|
|
27056
|
+
this.relayClient = transport;
|
|
27057
|
+
this.externalRelayTransport = true;
|
|
27058
|
+
}
|
|
26969
27059
|
getTasks() {
|
|
26970
27060
|
return this.tasks.values();
|
|
26971
27061
|
}
|
|
@@ -26981,6 +27071,9 @@ var A2AServer = class {
|
|
|
26981
27071
|
isRelayConnected() {
|
|
26982
27072
|
return this.relayClient?.isConnected() || false;
|
|
26983
27073
|
}
|
|
27074
|
+
getAgentCard() {
|
|
27075
|
+
return structuredClone(this.agentCard);
|
|
27076
|
+
}
|
|
26984
27077
|
authMiddleware() {
|
|
26985
27078
|
return (req, res, next) => {
|
|
26986
27079
|
if (!this.config.auth?.requireApiKey) {
|
|
@@ -27068,6 +27161,9 @@ var A2AServer = class {
|
|
|
27068
27161
|
});
|
|
27069
27162
|
}
|
|
27070
27163
|
async handleSendMessage(params, rpcId) {
|
|
27164
|
+
if (this.lifecycleState === "stopping" || this.lifecycleState === "stopped") {
|
|
27165
|
+
return this.error(rpcId, 503, "SERVER_RELOADING");
|
|
27166
|
+
}
|
|
27071
27167
|
const message = params.message;
|
|
27072
27168
|
const taskId = randomUUID4();
|
|
27073
27169
|
const contextId = message?.contextId || randomUUID4();
|
|
@@ -27088,10 +27184,11 @@ var A2AServer = class {
|
|
|
27088
27184
|
this.logger.info(
|
|
27089
27185
|
`[perkos-a2a] Task ${taskId} received from ${task.metadata?.fromAgent}`
|
|
27090
27186
|
);
|
|
27091
|
-
|
|
27187
|
+
const generation = this.generation;
|
|
27188
|
+
this.trackOperation(this.processTask(task, generation));
|
|
27092
27189
|
return this.success(rpcId, task);
|
|
27093
27190
|
}
|
|
27094
|
-
async processTask(task) {
|
|
27191
|
+
async processTask(task, generation) {
|
|
27095
27192
|
task.status = { state: "working", timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
27096
27193
|
this.tasks.set(task.id, structuredClone(task));
|
|
27097
27194
|
const textParts = task.messages.flatMap((m) => m.parts || []).filter((p) => p.kind === "text").map((p) => p.text).join("\n");
|
|
@@ -27135,9 +27232,11 @@ var A2AServer = class {
|
|
|
27135
27232
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
27136
27233
|
};
|
|
27137
27234
|
}
|
|
27235
|
+
if (!this.isGenerationActive(generation)) return;
|
|
27138
27236
|
this.tasks.set(task.id, structuredClone(task));
|
|
27139
27237
|
this.logger.info(`[perkos-a2a] Task ${task.id} completed`);
|
|
27140
27238
|
} catch (err) {
|
|
27239
|
+
if (!this.isGenerationActive(generation)) return;
|
|
27141
27240
|
const msg = err instanceof Error ? err.message : String(err);
|
|
27142
27241
|
if (this.taskFailureHandler) {
|
|
27143
27242
|
await this.taskFailureHandler(task, msg);
|
|
@@ -27237,7 +27336,7 @@ var A2AServer = class {
|
|
|
27237
27336
|
async waitForRemoteTaskTerminal(targetUrl, headers, taskId, timeoutMs = 45e3, pollMs = 1e3) {
|
|
27238
27337
|
const deadline = Date.now() + timeoutMs;
|
|
27239
27338
|
while (Date.now() < deadline) {
|
|
27240
|
-
await new Promise((
|
|
27339
|
+
await new Promise((resolve4) => setTimeout(resolve4, pollMs));
|
|
27241
27340
|
const response = await fetch(`${targetUrl}/a2a/jsonrpc`, {
|
|
27242
27341
|
method: "POST",
|
|
27243
27342
|
headers,
|
|
@@ -27261,7 +27360,7 @@ var A2AServer = class {
|
|
|
27261
27360
|
if (task && isTerminalTaskState(task.status?.state)) {
|
|
27262
27361
|
return this.success(randomUUID4(), structuredClone(task));
|
|
27263
27362
|
}
|
|
27264
|
-
await new Promise((
|
|
27363
|
+
await new Promise((resolve4) => setTimeout(resolve4, pollMs));
|
|
27265
27364
|
}
|
|
27266
27365
|
return null;
|
|
27267
27366
|
}
|
|
@@ -27324,10 +27423,17 @@ var A2AServer = class {
|
|
|
27324
27423
|
}
|
|
27325
27424
|
/** Handle an inbound task received via the relay */
|
|
27326
27425
|
handleRelayTask(msg) {
|
|
27426
|
+
if (this.lifecycleState !== "running") {
|
|
27427
|
+
this.relayClient?.sendTaskResponse(
|
|
27428
|
+
msg,
|
|
27429
|
+
this.error(String(msg.payload?.id ?? msg.id), 503, "SERVER_RELOADING")
|
|
27430
|
+
);
|
|
27431
|
+
return;
|
|
27432
|
+
}
|
|
27327
27433
|
const payload = msg.payload;
|
|
27328
27434
|
const params = payload.params || payload;
|
|
27329
27435
|
const rpcId = payload.id || msg.id;
|
|
27330
|
-
this.handleSendMessage(params, rpcId).then(async (response) => {
|
|
27436
|
+
const operation = this.handleSendMessage(params, rpcId).then(async (response) => {
|
|
27331
27437
|
const task = response.result;
|
|
27332
27438
|
const acceptQueued = task?.messages?.[0]?.metadata?.acceptQueued === true;
|
|
27333
27439
|
if (task?.id && !isTerminalTaskState(task.status?.state)) {
|
|
@@ -27358,10 +27464,32 @@ var A2AServer = class {
|
|
|
27358
27464
|
this.logger.error(`[perkos-a2a] Failed to process relay task: ${errMsg2}`);
|
|
27359
27465
|
this.relayClient?.sendTaskResponse(msg, this.error(rpcId, -32603, errMsg2));
|
|
27360
27466
|
});
|
|
27467
|
+
this.trackOperation(operation);
|
|
27361
27468
|
}
|
|
27362
|
-
|
|
27469
|
+
/** Process a relay task delivered by the OpenClaw supervisor over IPC. */
|
|
27470
|
+
async handleTransportTask(msg) {
|
|
27471
|
+
if (this.lifecycleState !== "running") {
|
|
27472
|
+
return this.error(String(msg.payload?.id ?? msg.id), 503, "SERVER_RELOADING");
|
|
27473
|
+
}
|
|
27474
|
+
const payload = msg.payload;
|
|
27475
|
+
const params = payload.params || payload;
|
|
27476
|
+
const rpcId = String(payload.id || msg.id);
|
|
27477
|
+
const response = await this.handleSendMessage(params, rpcId);
|
|
27478
|
+
const task = response.result;
|
|
27479
|
+
if (task?.id && !isTerminalTaskState(task.status?.state)) {
|
|
27480
|
+
const terminal = await this.waitForLocalTaskTerminal(task.id);
|
|
27481
|
+
if (terminal) return terminal;
|
|
27482
|
+
}
|
|
27483
|
+
return response;
|
|
27484
|
+
}
|
|
27485
|
+
async startRelayClient() {
|
|
27363
27486
|
const relay = this.config.relay;
|
|
27364
27487
|
if (!relay?.enabled || !relay?.url) return;
|
|
27488
|
+
if (this.externalRelayTransport && this.relayClient) {
|
|
27489
|
+
await this.relayClient.start();
|
|
27490
|
+
this.logger.info(`[perkos-a2a] Relay transport delegated to supervisor for ${relay.url}`);
|
|
27491
|
+
return;
|
|
27492
|
+
}
|
|
27365
27493
|
this.relayClient = new RelayClient({
|
|
27366
27494
|
agentName: this.config.agentName,
|
|
27367
27495
|
relay,
|
|
@@ -27389,15 +27517,32 @@ var A2AServer = class {
|
|
|
27389
27517
|
this.relayHub.start();
|
|
27390
27518
|
}
|
|
27391
27519
|
async start() {
|
|
27520
|
+
if (this.lifecycleState === "running") return;
|
|
27521
|
+
if (this.lifecycleState === "starting" && this.startPromise) return this.startPromise;
|
|
27522
|
+
if (this.lifecycleState === "stopping" && this.stopPromise) await this.stopPromise;
|
|
27523
|
+
const generation = ++this.generation;
|
|
27524
|
+
this.lifecycleState = "starting";
|
|
27525
|
+
const startPromise = this.startGeneration(generation);
|
|
27526
|
+
this.startPromise = startPromise;
|
|
27527
|
+
try {
|
|
27528
|
+
await startPromise;
|
|
27529
|
+
if (this.isGenerationActive(generation, "starting")) this.lifecycleState = "running";
|
|
27530
|
+
} finally {
|
|
27531
|
+
if (this.startPromise === startPromise) this.startPromise = null;
|
|
27532
|
+
}
|
|
27533
|
+
}
|
|
27534
|
+
async startGeneration(generation) {
|
|
27392
27535
|
const mode = this.config.mode || "auto";
|
|
27393
27536
|
if (mode === "relay") {
|
|
27537
|
+
if (!this.isGenerationActive(generation, "starting")) return;
|
|
27394
27538
|
this.clientOnly = true;
|
|
27395
27539
|
this.startRelayHub();
|
|
27396
27540
|
return;
|
|
27397
27541
|
}
|
|
27398
27542
|
if (mode === "client-only") {
|
|
27543
|
+
if (!this.isGenerationActive(generation, "starting")) return;
|
|
27399
27544
|
this.clientOnly = true;
|
|
27400
|
-
this.startRelayClient();
|
|
27545
|
+
await this.startRelayClient();
|
|
27401
27546
|
if (!this.config.relay?.enabled) {
|
|
27402
27547
|
this.logger.info(
|
|
27403
27548
|
"[perkos-a2a] Running in client-only mode. Configure relay for NAT traversal or set up Tailscale/tunnel."
|
|
@@ -27406,19 +27551,21 @@ var A2AServer = class {
|
|
|
27406
27551
|
return;
|
|
27407
27552
|
}
|
|
27408
27553
|
if (mode === "full") {
|
|
27409
|
-
this.tryListen(this.config.port);
|
|
27410
|
-
this.
|
|
27554
|
+
await this.tryListen(this.config.port, 1, generation);
|
|
27555
|
+
if (!this.isGenerationActive(generation, "starting")) return;
|
|
27556
|
+
await this.startRelayClient();
|
|
27411
27557
|
return;
|
|
27412
27558
|
}
|
|
27413
27559
|
try {
|
|
27414
|
-
const
|
|
27415
|
-
if (
|
|
27560
|
+
const net2 = await detectNetworking();
|
|
27561
|
+
if (!this.isGenerationActive(generation, "starting")) return;
|
|
27562
|
+
if (net2.isBehindNat && !net2.hasTailscale) {
|
|
27416
27563
|
if (this.config.relay?.enabled) {
|
|
27417
27564
|
this.logger.info(
|
|
27418
27565
|
"[perkos-a2a] Behind NAT, using relay for bidirectional communication"
|
|
27419
27566
|
);
|
|
27420
27567
|
this.clientOnly = true;
|
|
27421
|
-
this.startRelayClient();
|
|
27568
|
+
await this.startRelayClient();
|
|
27422
27569
|
} else {
|
|
27423
27570
|
this.logger.info(
|
|
27424
27571
|
"[perkos-a2a] Running in client-only mode (behind NAT). Configure relay or Tailscale for bidirectional A2A."
|
|
@@ -27427,44 +27574,136 @@ var A2AServer = class {
|
|
|
27427
27574
|
}
|
|
27428
27575
|
return;
|
|
27429
27576
|
}
|
|
27430
|
-
if (
|
|
27577
|
+
if (net2.isBehindNat && net2.hasTailscale && net2.tailscaleIp) {
|
|
27431
27578
|
this.logger.info(
|
|
27432
|
-
`[perkos-a2a] Behind NAT but Tailscale detected (${
|
|
27579
|
+
`[perkos-a2a] Behind NAT but Tailscale detected (${net2.tailscaleIp}). Starting server.`
|
|
27433
27580
|
);
|
|
27434
27581
|
}
|
|
27435
27582
|
} catch {
|
|
27436
27583
|
}
|
|
27437
|
-
this.tryListen(this.config.port);
|
|
27438
|
-
this.
|
|
27584
|
+
await this.tryListen(this.config.port, 1, generation);
|
|
27585
|
+
if (!this.isGenerationActive(generation, "starting")) return;
|
|
27586
|
+
await this.startRelayClient();
|
|
27587
|
+
}
|
|
27588
|
+
stop(options = {}) {
|
|
27589
|
+
if (this.lifecycleState === "stopping" && this.stopPromise) return this.stopPromise;
|
|
27590
|
+
if (this.lifecycleState === "stopped" || this.lifecycleState === "new") {
|
|
27591
|
+
this.lifecycleState = "stopped";
|
|
27592
|
+
this.generation += 1;
|
|
27593
|
+
return Promise.resolve();
|
|
27594
|
+
}
|
|
27595
|
+
this.lifecycleState = "stopping";
|
|
27596
|
+
this.generation += 1;
|
|
27597
|
+
const priorStart = this.startPromise;
|
|
27598
|
+
let stopPromise;
|
|
27599
|
+
stopPromise = (async () => {
|
|
27600
|
+
try {
|
|
27601
|
+
const timeoutMs = options.timeoutMs ?? 5e3;
|
|
27602
|
+
const drainMs = Math.min(options.drainMs ?? 3e3, timeoutMs);
|
|
27603
|
+
await this.teardownResources(timeoutMs);
|
|
27604
|
+
await priorStart?.catch(() => void 0);
|
|
27605
|
+
await this.teardownResources(timeoutMs);
|
|
27606
|
+
await this.drainOperations(drainMs);
|
|
27607
|
+
for (const task of this.tasks.values()) {
|
|
27608
|
+
if (!isTerminalTaskState(task.status.state)) {
|
|
27609
|
+
task.status = {
|
|
27610
|
+
state: "failed",
|
|
27611
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
27612
|
+
message: { role: "agent", parts: [{ kind: "text", text: "interrupted_by_reload" }] }
|
|
27613
|
+
};
|
|
27614
|
+
}
|
|
27615
|
+
}
|
|
27616
|
+
this.lifecycleState = "stopped";
|
|
27617
|
+
this.logger.info(`[perkos-a2a] A2A server stopped${options.reason ? ` (${options.reason})` : ""}`);
|
|
27618
|
+
} finally {
|
|
27619
|
+
if (this.stopPromise === stopPromise) this.stopPromise = null;
|
|
27620
|
+
}
|
|
27621
|
+
})();
|
|
27622
|
+
this.stopPromise = stopPromise;
|
|
27623
|
+
return stopPromise;
|
|
27439
27624
|
}
|
|
27440
|
-
tryListen(port, attempt
|
|
27625
|
+
tryListen(port, attempt, generation) {
|
|
27441
27626
|
const maxAttempts = 3;
|
|
27442
|
-
|
|
27627
|
+
return new Promise((resolveStart) => {
|
|
27628
|
+
if (!this.isGenerationActive(generation, "starting")) {
|
|
27629
|
+
resolveStart();
|
|
27630
|
+
return;
|
|
27631
|
+
}
|
|
27443
27632
|
const bindHost = this.config.bindHost || "0.0.0.0";
|
|
27444
27633
|
const srv = this.app.listen(port, bindHost, () => {
|
|
27634
|
+
if (!this.isGenerationActive(generation, "starting")) {
|
|
27635
|
+
srv.close(() => resolveStart());
|
|
27636
|
+
return;
|
|
27637
|
+
}
|
|
27638
|
+
this.httpServer = srv;
|
|
27445
27639
|
this.logger.info(
|
|
27446
27640
|
`[perkos-a2a] ${this.config.agentName} server on ${bindHost}:${port}`
|
|
27447
27641
|
);
|
|
27448
27642
|
this.agentCard.url = `${(this.config.publicUrl || `http://localhost:${port}`).replace(/\/+$/, "")}/a2a/jsonrpc`;
|
|
27643
|
+
resolveStart();
|
|
27449
27644
|
});
|
|
27450
27645
|
srv.on("error", (err) => {
|
|
27451
27646
|
if (err.code === "EADDRINUSE" && attempt < maxAttempts) {
|
|
27452
27647
|
const nextPort = port + 1;
|
|
27453
27648
|
this.logger.info(`[perkos-a2a] Port ${port} in use, trying ${nextPort}`);
|
|
27454
|
-
this.tryListen(nextPort, attempt + 1);
|
|
27649
|
+
void this.tryListen(nextPort, attempt + 1, generation).then(resolveStart);
|
|
27455
27650
|
} else if (err.code === "EADDRINUSE") {
|
|
27456
27651
|
this.logger.info(
|
|
27457
27652
|
`[perkos-a2a] Ports ${this.config.port}-${port} all in use. Falling back to client-only mode.`
|
|
27458
27653
|
);
|
|
27459
27654
|
this.clientOnly = true;
|
|
27655
|
+
resolveStart();
|
|
27460
27656
|
} else {
|
|
27461
27657
|
this.logger.error(`[perkos-a2a] Server error: ${err.message}`);
|
|
27658
|
+
resolveStart();
|
|
27462
27659
|
}
|
|
27463
27660
|
});
|
|
27464
|
-
}
|
|
27465
|
-
|
|
27466
|
-
|
|
27467
|
-
|
|
27661
|
+
});
|
|
27662
|
+
}
|
|
27663
|
+
isGenerationActive(generation, expectedState) {
|
|
27664
|
+
return generation === this.generation && (expectedState ? this.lifecycleState === expectedState : this.lifecycleState !== "stopping" && this.lifecycleState !== "stopped");
|
|
27665
|
+
}
|
|
27666
|
+
trackOperation(operation) {
|
|
27667
|
+
this.operations.add(operation);
|
|
27668
|
+
void operation.finally(() => this.operations.delete(operation)).catch(() => void 0);
|
|
27669
|
+
}
|
|
27670
|
+
async drainOperations(timeoutMs) {
|
|
27671
|
+
if (this.operations.size === 0 || timeoutMs <= 0) return;
|
|
27672
|
+
await Promise.race([
|
|
27673
|
+
Promise.allSettled([...this.operations]),
|
|
27674
|
+
new Promise((resolveDrain) => {
|
|
27675
|
+
const timer = setTimeout(resolveDrain, timeoutMs);
|
|
27676
|
+
timer.unref?.();
|
|
27677
|
+
})
|
|
27678
|
+
]);
|
|
27679
|
+
}
|
|
27680
|
+
async teardownResources(timeoutMs) {
|
|
27681
|
+
const deadline = Date.now() + timeoutMs;
|
|
27682
|
+
const remaining = () => Math.max(1, deadline - Date.now());
|
|
27683
|
+
const relayClient = this.relayClient;
|
|
27684
|
+
if (!this.externalRelayTransport) this.relayClient = null;
|
|
27685
|
+
await relayClient?.stopAndWait(remaining());
|
|
27686
|
+
const relayHub = this.relayHub;
|
|
27687
|
+
this.relayHub = null;
|
|
27688
|
+
await relayHub?.stop(remaining());
|
|
27689
|
+
const httpServer = this.httpServer;
|
|
27690
|
+
this.httpServer = null;
|
|
27691
|
+
if (!httpServer) return;
|
|
27692
|
+
await new Promise((resolveClose) => {
|
|
27693
|
+
let settled = false;
|
|
27694
|
+
const done = () => {
|
|
27695
|
+
if (settled) return;
|
|
27696
|
+
settled = true;
|
|
27697
|
+
clearTimeout(timer);
|
|
27698
|
+
resolveClose();
|
|
27699
|
+
};
|
|
27700
|
+
const timer = setTimeout(() => {
|
|
27701
|
+
httpServer.closeAllConnections?.();
|
|
27702
|
+
done();
|
|
27703
|
+
}, remaining());
|
|
27704
|
+
timer.unref?.();
|
|
27705
|
+
httpServer.close(() => done());
|
|
27706
|
+
});
|
|
27468
27707
|
}
|
|
27469
27708
|
getExpressApp() {
|
|
27470
27709
|
return this.app;
|
|
@@ -27472,10 +27711,10 @@ var A2AServer = class {
|
|
|
27472
27711
|
};
|
|
27473
27712
|
|
|
27474
27713
|
// src/index.ts
|
|
27475
|
-
import { randomUUID as
|
|
27476
|
-
import { readFileSync as
|
|
27477
|
-
import { dirname, resolve as
|
|
27478
|
-
import { fileURLToPath } from "node:url";
|
|
27714
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
27715
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
27716
|
+
import { dirname as dirname2, resolve as resolve3 } from "node:path";
|
|
27717
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
27479
27718
|
|
|
27480
27719
|
// src/agentic-actions.ts
|
|
27481
27720
|
var FALLBACK_AGENT_ALIASES = [
|
|
@@ -27890,9 +28129,6 @@ ${replyResultText2}` : void 0,
|
|
|
27890
28129
|
return true;
|
|
27891
28130
|
}
|
|
27892
28131
|
|
|
27893
|
-
// src/chat-client.ts
|
|
27894
|
-
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
27895
|
-
|
|
27896
28132
|
// src/chat-store.ts
|
|
27897
28133
|
import { createHash } from "node:crypto";
|
|
27898
28134
|
import { createReadStream } from "node:fs";
|
|
@@ -28020,7 +28256,7 @@ var ChatStore = class {
|
|
|
28020
28256
|
let firstMessageAt = null;
|
|
28021
28257
|
let lastMessageAt = null;
|
|
28022
28258
|
let leftover = "";
|
|
28023
|
-
return new Promise((
|
|
28259
|
+
return new Promise((resolve4, reject) => {
|
|
28024
28260
|
stream.on("data", (chunk) => {
|
|
28025
28261
|
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
28026
28262
|
hash.update(text);
|
|
@@ -28060,7 +28296,7 @@ var ChatStore = class {
|
|
|
28060
28296
|
hash.update(metaRaw);
|
|
28061
28297
|
} catch {
|
|
28062
28298
|
}
|
|
28063
|
-
|
|
28299
|
+
resolve4({
|
|
28064
28300
|
transcriptHash: hash.digest("hex"),
|
|
28065
28301
|
hashAlgo: "sha256",
|
|
28066
28302
|
messageCount,
|
|
@@ -28095,7 +28331,398 @@ function isNotFound(err) {
|
|
|
28095
28331
|
return !!err && typeof err === "object" && err.code === "ENOENT";
|
|
28096
28332
|
}
|
|
28097
28333
|
|
|
28334
|
+
// src/openclaw-supervisor-client.ts
|
|
28335
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
28336
|
+
import { spawn } from "node:child_process";
|
|
28337
|
+
import { homedir as homedir3 } from "node:os";
|
|
28338
|
+
import net from "node:net";
|
|
28339
|
+
import { resolve } from "node:path";
|
|
28340
|
+
import { fileURLToPath } from "node:url";
|
|
28341
|
+
|
|
28342
|
+
// src/supervisor-protocol.ts
|
|
28343
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
28344
|
+
import { chmodSync, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "node:fs";
|
|
28345
|
+
import { tmpdir } from "node:os";
|
|
28346
|
+
import { dirname, join as join2 } from "node:path";
|
|
28347
|
+
var SUPERVISOR_PROTOCOL_VERSION = 1;
|
|
28348
|
+
var SUPERVISOR_RENEW_MS = 5e3;
|
|
28349
|
+
function supervisorOwnerHash(lifecycleKey) {
|
|
28350
|
+
return createHash2("sha256").update(lifecycleKey).digest("hex");
|
|
28351
|
+
}
|
|
28352
|
+
function supervisorDir(stateDir) {
|
|
28353
|
+
return join2(stateDir, "perkos-a2a", "supervisors");
|
|
28354
|
+
}
|
|
28355
|
+
function supervisorSocketPath(stateDir, ownerHash) {
|
|
28356
|
+
const preferred = join2(supervisorDir(stateDir), `${ownerHash.slice(0, 32)}.sock`);
|
|
28357
|
+
if (Buffer.byteLength(preferred) <= 96) return preferred;
|
|
28358
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : "user";
|
|
28359
|
+
return join2(tmpdir(), `perkos-a2a-${uid}`, `${ownerHash.slice(0, 32)}.sock`);
|
|
28360
|
+
}
|
|
28361
|
+
function currentBootId() {
|
|
28362
|
+
try {
|
|
28363
|
+
return readFileSync2("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
28364
|
+
} catch {
|
|
28365
|
+
return `process-boot-${Math.floor(Date.now() - process.uptime() * 1e3)}`;
|
|
28366
|
+
}
|
|
28367
|
+
}
|
|
28368
|
+
function processStartIdentity(pid = process.pid) {
|
|
28369
|
+
try {
|
|
28370
|
+
const stat = readFileSync2(`/proc/${pid}/stat`, "utf8");
|
|
28371
|
+
const afterName = stat.slice(stat.lastIndexOf(")") + 2).trim().split(/\s+/u);
|
|
28372
|
+
return `linux-start-ticks:${afterName[19] ?? "unknown"}`;
|
|
28373
|
+
} catch {
|
|
28374
|
+
if (pid === process.pid) return `epoch-ms:${Math.floor(Date.now() - process.uptime() * 1e3)}`;
|
|
28375
|
+
return "unknown";
|
|
28376
|
+
}
|
|
28377
|
+
}
|
|
28378
|
+
function encodeSupervisorMessage(message) {
|
|
28379
|
+
return `${JSON.stringify(message)}
|
|
28380
|
+
`;
|
|
28381
|
+
}
|
|
28382
|
+
|
|
28383
|
+
// src/openclaw-supervisor-client.ts
|
|
28384
|
+
var OpenClawSupervisorClient = class {
|
|
28385
|
+
stateDir;
|
|
28386
|
+
ownerHash;
|
|
28387
|
+
socketPath;
|
|
28388
|
+
config;
|
|
28389
|
+
logger;
|
|
28390
|
+
handlers;
|
|
28391
|
+
lease;
|
|
28392
|
+
socket = null;
|
|
28393
|
+
startPromise = null;
|
|
28394
|
+
stopPromise = null;
|
|
28395
|
+
renewTimer = null;
|
|
28396
|
+
pending = /* @__PURE__ */ new Map();
|
|
28397
|
+
health = null;
|
|
28398
|
+
stopped = true;
|
|
28399
|
+
constructor(input) {
|
|
28400
|
+
this.stateDir = resolve(input.stateDir ?? process.env.OPENCLAW_STATE_DIR ?? `${homedir3()}/.openclaw`);
|
|
28401
|
+
this.ownerHash = input.ownerHash;
|
|
28402
|
+
this.socketPath = supervisorSocketPath(this.stateDir, this.ownerHash);
|
|
28403
|
+
this.config = input.config;
|
|
28404
|
+
this.logger = input.logger;
|
|
28405
|
+
this.handlers = input.handlers;
|
|
28406
|
+
this.lease = {
|
|
28407
|
+
ownerKey: input.ownerHash,
|
|
28408
|
+
instanceId: input.instanceId,
|
|
28409
|
+
generation: input.generation,
|
|
28410
|
+
parentPid: process.pid,
|
|
28411
|
+
parentStartTime: processStartIdentity(),
|
|
28412
|
+
bootId: currentBootId(),
|
|
28413
|
+
lastRenewedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
28414
|
+
};
|
|
28415
|
+
}
|
|
28416
|
+
isConnected() {
|
|
28417
|
+
return this.health?.relayConnected === true && this.health?.relayRegistered === true;
|
|
28418
|
+
}
|
|
28419
|
+
isChatConnected() {
|
|
28420
|
+
return this.health?.chatConnected === true;
|
|
28421
|
+
}
|
|
28422
|
+
currentHealth() {
|
|
28423
|
+
return this.health;
|
|
28424
|
+
}
|
|
28425
|
+
start() {
|
|
28426
|
+
if (!this.stopped && this.socket && !this.socket.destroyed) return Promise.resolve();
|
|
28427
|
+
if (this.startPromise) return this.startPromise;
|
|
28428
|
+
this.stopped = false;
|
|
28429
|
+
const starting = (async () => {
|
|
28430
|
+
await this.connectOrSpawn();
|
|
28431
|
+
await this.requestWithMessage({
|
|
28432
|
+
protocolVersion: 1,
|
|
28433
|
+
type: "claim",
|
|
28434
|
+
requestId: randomUUID5(),
|
|
28435
|
+
lease: this.freshLease(),
|
|
28436
|
+
config: this.config
|
|
28437
|
+
}, 15e3);
|
|
28438
|
+
this.renewTimer = setInterval(() => this.renew(), SUPERVISOR_RENEW_MS);
|
|
28439
|
+
this.renewTimer.unref?.();
|
|
28440
|
+
this.logger.info(`[perkos-a2a] Supervisor owner ready generation=${this.lease.generation} instanceId=${this.lease.instanceId}`);
|
|
28441
|
+
})().finally(() => {
|
|
28442
|
+
if (this.startPromise === starting) this.startPromise = null;
|
|
28443
|
+
});
|
|
28444
|
+
this.startPromise = starting;
|
|
28445
|
+
return starting;
|
|
28446
|
+
}
|
|
28447
|
+
stop() {
|
|
28448
|
+
if (this.stopPromise) return this.stopPromise;
|
|
28449
|
+
if (this.stopped && !this.socket) return Promise.resolve();
|
|
28450
|
+
this.stopped = true;
|
|
28451
|
+
if (this.renewTimer) clearInterval(this.renewTimer);
|
|
28452
|
+
this.renewTimer = null;
|
|
28453
|
+
const stopping = (async () => {
|
|
28454
|
+
if (this.socket && !this.socket.destroyed) {
|
|
28455
|
+
const requestId = randomUUID5();
|
|
28456
|
+
try {
|
|
28457
|
+
await this.requestWithMessage({
|
|
28458
|
+
protocolVersion: 1,
|
|
28459
|
+
type: "release",
|
|
28460
|
+
requestId,
|
|
28461
|
+
lease: this.freshLease()
|
|
28462
|
+
}, 8e3);
|
|
28463
|
+
} catch (error) {
|
|
28464
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
28465
|
+
if (!message.includes("NOT_OWNER") && !message.includes("SUPERVISOR_DISCONNECTED")) throw error;
|
|
28466
|
+
}
|
|
28467
|
+
}
|
|
28468
|
+
this.closeSocket(new Error("CLIENT_STOPPED"));
|
|
28469
|
+
this.logger.info(`[perkos-a2a] A2A server stopped generation=${this.lease.generation} instanceId=${this.lease.instanceId}`);
|
|
28470
|
+
})().finally(() => {
|
|
28471
|
+
if (this.stopPromise === stopping) this.stopPromise = null;
|
|
28472
|
+
});
|
|
28473
|
+
this.stopPromise = stopping;
|
|
28474
|
+
return stopping;
|
|
28475
|
+
}
|
|
28476
|
+
async stopAndWait() {
|
|
28477
|
+
await this.stop();
|
|
28478
|
+
return { closeTimedOut: false };
|
|
28479
|
+
}
|
|
28480
|
+
async sendTask(targetAgent, payload, taskId) {
|
|
28481
|
+
return await this.request("sendTask", { target: targetAgent, payload, ...taskId ? { taskId } : {} }, 7e4);
|
|
28482
|
+
}
|
|
28483
|
+
async discover() {
|
|
28484
|
+
return await this.request("discover", {}, 15e3);
|
|
28485
|
+
}
|
|
28486
|
+
sendTaskResponse(original, result) {
|
|
28487
|
+
this.write({ protocolVersion: 1, type: "relayTaskResult", lease: this.freshLease(), eventId: randomUUID5(), original, result });
|
|
28488
|
+
}
|
|
28489
|
+
async sendChatReply(payload) {
|
|
28490
|
+
return await this.request("chatReply", payload, 15e3);
|
|
28491
|
+
}
|
|
28492
|
+
async request(method, payload, timeoutMs) {
|
|
28493
|
+
const requestId = randomUUID5();
|
|
28494
|
+
return this.requestWithMessage({ protocolVersion: 1, type: "request", requestId, lease: this.freshLease(), method, payload }, timeoutMs);
|
|
28495
|
+
}
|
|
28496
|
+
async requestWithMessage(message, timeoutMs) {
|
|
28497
|
+
return new Promise((resolveRequest, rejectRequest) => {
|
|
28498
|
+
const timer = setTimeout(() => {
|
|
28499
|
+
this.pending.delete(message.requestId);
|
|
28500
|
+
rejectRequest(new Error(`SUPERVISOR_REQUEST_TIMEOUT:${message.type}`));
|
|
28501
|
+
}, timeoutMs);
|
|
28502
|
+
timer.unref?.();
|
|
28503
|
+
this.pending.set(message.requestId, { resolve: resolveRequest, reject: rejectRequest, timer });
|
|
28504
|
+
try {
|
|
28505
|
+
this.write(message);
|
|
28506
|
+
} catch (error) {
|
|
28507
|
+
clearTimeout(timer);
|
|
28508
|
+
this.pending.delete(message.requestId);
|
|
28509
|
+
rejectRequest(error instanceof Error ? error : new Error(String(error)));
|
|
28510
|
+
}
|
|
28511
|
+
});
|
|
28512
|
+
}
|
|
28513
|
+
freshLease() {
|
|
28514
|
+
this.lease.lastRenewedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
28515
|
+
return { ...this.lease };
|
|
28516
|
+
}
|
|
28517
|
+
renew() {
|
|
28518
|
+
if (this.stopped || !this.socket || this.socket.destroyed) return;
|
|
28519
|
+
this.write({ protocolVersion: 1, type: "renew", lease: this.freshLease() });
|
|
28520
|
+
}
|
|
28521
|
+
async connectOrSpawn() {
|
|
28522
|
+
try {
|
|
28523
|
+
await this.connectSocket(1e3);
|
|
28524
|
+
return;
|
|
28525
|
+
} catch {
|
|
28526
|
+
}
|
|
28527
|
+
const cliPath = fileURLToPath(new URL("./openclaw-supervisor-cli.js", import.meta.url));
|
|
28528
|
+
const child = spawn(process.execPath, [cliPath, "--state-dir", this.stateDir, "--owner-key", this.ownerHash], {
|
|
28529
|
+
detached: true,
|
|
28530
|
+
stdio: "ignore",
|
|
28531
|
+
env: { ...process.env }
|
|
28532
|
+
});
|
|
28533
|
+
child.unref();
|
|
28534
|
+
const deadline = Date.now() + 1e4;
|
|
28535
|
+
let lastError;
|
|
28536
|
+
while (Date.now() < deadline) {
|
|
28537
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 100));
|
|
28538
|
+
try {
|
|
28539
|
+
await this.connectSocket(1e3);
|
|
28540
|
+
return;
|
|
28541
|
+
} catch (error) {
|
|
28542
|
+
lastError = error;
|
|
28543
|
+
}
|
|
28544
|
+
}
|
|
28545
|
+
throw new Error(`OWNERSHIP_PRIMITIVE_UNAVAILABLE:${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
|
28546
|
+
}
|
|
28547
|
+
connectSocket(timeoutMs) {
|
|
28548
|
+
this.closeSocket(new Error("SUPERVISOR_RECONNECT"));
|
|
28549
|
+
return new Promise((resolveConnect, rejectConnect) => {
|
|
28550
|
+
const socket = net.createConnection(this.socketPath);
|
|
28551
|
+
socket.setEncoding("utf8");
|
|
28552
|
+
let buffered = "";
|
|
28553
|
+
let settled = false;
|
|
28554
|
+
const timer = setTimeout(() => {
|
|
28555
|
+
if (settled) return;
|
|
28556
|
+
settled = true;
|
|
28557
|
+
socket.destroy();
|
|
28558
|
+
rejectConnect(new Error("supervisor socket connect timeout"));
|
|
28559
|
+
}, timeoutMs);
|
|
28560
|
+
timer.unref?.();
|
|
28561
|
+
socket.once("connect", () => {
|
|
28562
|
+
if (settled) return;
|
|
28563
|
+
settled = true;
|
|
28564
|
+
clearTimeout(timer);
|
|
28565
|
+
this.socket = socket;
|
|
28566
|
+
resolveConnect();
|
|
28567
|
+
});
|
|
28568
|
+
socket.once("error", (error) => {
|
|
28569
|
+
if (!settled) {
|
|
28570
|
+
settled = true;
|
|
28571
|
+
clearTimeout(timer);
|
|
28572
|
+
rejectConnect(error);
|
|
28573
|
+
}
|
|
28574
|
+
});
|
|
28575
|
+
socket.on("data", (chunk) => {
|
|
28576
|
+
buffered += chunk;
|
|
28577
|
+
for (; ; ) {
|
|
28578
|
+
const newline = buffered.indexOf("\n");
|
|
28579
|
+
if (newline < 0) break;
|
|
28580
|
+
const line = buffered.slice(0, newline);
|
|
28581
|
+
buffered = buffered.slice(newline + 1);
|
|
28582
|
+
if (line.trim()) void this.handleEvent(JSON.parse(line));
|
|
28583
|
+
}
|
|
28584
|
+
});
|
|
28585
|
+
socket.on("close", () => {
|
|
28586
|
+
if (this.socket === socket) this.closeSocket(new Error("SUPERVISOR_DISCONNECTED"));
|
|
28587
|
+
});
|
|
28588
|
+
});
|
|
28589
|
+
}
|
|
28590
|
+
async handleEvent(event) {
|
|
28591
|
+
if (event.protocolVersion !== SUPERVISOR_PROTOCOL_VERSION) return;
|
|
28592
|
+
if (event.type === "response") {
|
|
28593
|
+
const pending = this.pending.get(event.requestId);
|
|
28594
|
+
if (!pending) return;
|
|
28595
|
+
this.pending.delete(event.requestId);
|
|
28596
|
+
clearTimeout(pending.timer);
|
|
28597
|
+
if (event.ok) pending.resolve(event.result);
|
|
28598
|
+
else pending.reject(new Error(`${event.error?.code || "SUPERVISOR_ERROR"}:${event.error?.message || "request failed"}`));
|
|
28599
|
+
return;
|
|
28600
|
+
}
|
|
28601
|
+
if (event.type === "health") {
|
|
28602
|
+
this.health = event.health;
|
|
28603
|
+
return;
|
|
28604
|
+
}
|
|
28605
|
+
if (event.type === "log") {
|
|
28606
|
+
(event.level === "error" ? this.logger.error : this.logger.info)(event.message);
|
|
28607
|
+
return;
|
|
28608
|
+
}
|
|
28609
|
+
if (event.type === "relayTask") {
|
|
28610
|
+
try {
|
|
28611
|
+
const result = await this.handlers.onRelayTask(event.message);
|
|
28612
|
+
this.write({ protocolVersion: 1, type: "relayTaskResult", lease: this.freshLease(), eventId: event.eventId, original: event.message, result });
|
|
28613
|
+
} catch (error) {
|
|
28614
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
28615
|
+
this.write({
|
|
28616
|
+
protocolVersion: 1,
|
|
28617
|
+
type: "relayTaskResult",
|
|
28618
|
+
lease: this.freshLease(),
|
|
28619
|
+
eventId: event.eventId,
|
|
28620
|
+
original: event.message,
|
|
28621
|
+
result: { jsonrpc: "2.0", id: event.message.id, error: { code: -32603, message } }
|
|
28622
|
+
});
|
|
28623
|
+
}
|
|
28624
|
+
return;
|
|
28625
|
+
}
|
|
28626
|
+
if (event.type === "chatDeliver") await this.handlers.onChatDeliver(event.frame);
|
|
28627
|
+
if (event.type === "channelJoin") await this.handlers.onChannelJoin(event.frame);
|
|
28628
|
+
}
|
|
28629
|
+
write(message) {
|
|
28630
|
+
if (!this.socket || this.socket.destroyed) throw new Error("SUPERVISOR_DISCONNECTED");
|
|
28631
|
+
this.socket.write(encodeSupervisorMessage(message));
|
|
28632
|
+
}
|
|
28633
|
+
closeSocket(error) {
|
|
28634
|
+
if (this.renewTimer) clearInterval(this.renewTimer);
|
|
28635
|
+
this.renewTimer = null;
|
|
28636
|
+
const socket = this.socket;
|
|
28637
|
+
this.socket = null;
|
|
28638
|
+
if (socket && !socket.destroyed) socket.destroy();
|
|
28639
|
+
for (const pending of this.pending.values()) {
|
|
28640
|
+
clearTimeout(pending.timer);
|
|
28641
|
+
pending.reject(error);
|
|
28642
|
+
}
|
|
28643
|
+
this.pending.clear();
|
|
28644
|
+
}
|
|
28645
|
+
};
|
|
28646
|
+
|
|
28647
|
+
// src/runtime-evidence.ts
|
|
28648
|
+
import { createHmac } from "node:crypto";
|
|
28649
|
+
import {
|
|
28650
|
+
chmodSync as chmodSync2,
|
|
28651
|
+
mkdirSync as mkdirSync2,
|
|
28652
|
+
readFileSync as readFileSync3,
|
|
28653
|
+
renameSync as renameSync2,
|
|
28654
|
+
writeFileSync as writeFileSync2
|
|
28655
|
+
} from "node:fs";
|
|
28656
|
+
import { homedir as homedir4 } from "node:os";
|
|
28657
|
+
import { join as join3, resolve as resolve2 } from "node:path";
|
|
28658
|
+
var RUNTIME_EVIDENCE_SCHEMA_VERSION = 1;
|
|
28659
|
+
function canonicalize(value) {
|
|
28660
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
28661
|
+
if (!value || typeof value !== "object") return value;
|
|
28662
|
+
return Object.fromEntries(
|
|
28663
|
+
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, child]) => [key, canonicalize(child)])
|
|
28664
|
+
);
|
|
28665
|
+
}
|
|
28666
|
+
function canonicalConfigJson(config) {
|
|
28667
|
+
return JSON.stringify({ schemaVersion: RUNTIME_EVIDENCE_SCHEMA_VERSION, config: canonicalize(config) });
|
|
28668
|
+
}
|
|
28669
|
+
function configFingerprint(config, hmacKeyHex) {
|
|
28670
|
+
if (!/^[a-f0-9]{64}$/u.test(hmacKeyHex)) throw new Error("runtime evidence HMAC key is invalid");
|
|
28671
|
+
return createHmac("sha256", Buffer.from(hmacKeyHex, "hex")).update(canonicalConfigJson(config)).digest("hex");
|
|
28672
|
+
}
|
|
28673
|
+
function runtimeEvidenceDir(stateDir) {
|
|
28674
|
+
return resolve2(stateDir ?? process.env.OPENCLAW_STATE_DIR ?? join3(homedir4(), ".openclaw"), "perkos-a2a");
|
|
28675
|
+
}
|
|
28676
|
+
function runtimeAttemptPath(stateDir) {
|
|
28677
|
+
return join3(runtimeEvidenceDir(stateDir), "active-attempt.json");
|
|
28678
|
+
}
|
|
28679
|
+
function runtimeStatusPath(stateDir) {
|
|
28680
|
+
return join3(runtimeEvidenceDir(stateDir), "runtime-status.json");
|
|
28681
|
+
}
|
|
28682
|
+
function configAgentId(config) {
|
|
28683
|
+
if (config.platform?.agentId) return config.platform.agentId;
|
|
28684
|
+
const match = config.platform?.heartbeatUrl?.match(/\/agents\/([^/]+)\/heartbeat/u);
|
|
28685
|
+
return match?.[1] ?? "unknown";
|
|
28686
|
+
}
|
|
28687
|
+
function recordRuntimeLoadEvidence(input) {
|
|
28688
|
+
const evidenceDir = runtimeEvidenceDir(input.stateDir);
|
|
28689
|
+
let attempt;
|
|
28690
|
+
try {
|
|
28691
|
+
attempt = JSON.parse(readFileSync3(runtimeAttemptPath(input.stateDir), "utf8"));
|
|
28692
|
+
} catch {
|
|
28693
|
+
input.logger.info(`[perkos-a2a] config loaded version=${input.version} agentId=${configAgentId(input.config)} attemptId=none`);
|
|
28694
|
+
return null;
|
|
28695
|
+
}
|
|
28696
|
+
const agentId = configAgentId(input.config);
|
|
28697
|
+
if (attempt.schemaVersion !== RUNTIME_EVIDENCE_SCHEMA_VERSION || attempt.targetVersion !== input.version || attempt.expectedAgentId !== agentId) {
|
|
28698
|
+
input.logger.warn?.("[perkos-a2a] runtime evidence attempt does not match the loaded plugin identity");
|
|
28699
|
+
return null;
|
|
28700
|
+
}
|
|
28701
|
+
const status = {
|
|
28702
|
+
schemaVersion: RUNTIME_EVIDENCE_SCHEMA_VERSION,
|
|
28703
|
+
loadedAt: (input.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
28704
|
+
version: input.version,
|
|
28705
|
+
agentId,
|
|
28706
|
+
fingerprint: configFingerprint(input.config, attempt.hmacKeyHex),
|
|
28707
|
+
attemptId: attempt.attemptId,
|
|
28708
|
+
pid: process.pid,
|
|
28709
|
+
pluginRoot: resolve2(input.pluginRoot)
|
|
28710
|
+
};
|
|
28711
|
+
mkdirSync2(evidenceDir, { recursive: true, mode: 448 });
|
|
28712
|
+
const target = runtimeStatusPath(input.stateDir);
|
|
28713
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
28714
|
+
writeFileSync2(temporary, `${JSON.stringify(status, null, 2)}
|
|
28715
|
+
`, { mode: 384 });
|
|
28716
|
+
renameSync2(temporary, target);
|
|
28717
|
+
chmodSync2(target, 384);
|
|
28718
|
+
input.logger.info(
|
|
28719
|
+
`[perkos-a2a] config loaded version=${status.version} agentId=${status.agentId} attemptId=${status.attemptId} fingerprint=${status.fingerprint}`
|
|
28720
|
+
);
|
|
28721
|
+
return status;
|
|
28722
|
+
}
|
|
28723
|
+
|
|
28098
28724
|
// src/chat-client.ts
|
|
28725
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
28099
28726
|
var DEFAULT_URL = "wss://chat.perkos.xyz/chat";
|
|
28100
28727
|
var DEFAULT_HISTORY_LIMIT = 50;
|
|
28101
28728
|
var DEFAULT_MIN_RECONNECT = 1e3;
|
|
@@ -28118,6 +28745,7 @@ var ChatClient = class {
|
|
|
28118
28745
|
connected = false;
|
|
28119
28746
|
authed = false;
|
|
28120
28747
|
stopped = false;
|
|
28748
|
+
generation = 0;
|
|
28121
28749
|
constructor(opts) {
|
|
28122
28750
|
this.agentName = opts.agentName;
|
|
28123
28751
|
const cfg = opts.config;
|
|
@@ -28153,20 +28781,43 @@ var ChatClient = class {
|
|
|
28153
28781
|
return;
|
|
28154
28782
|
}
|
|
28155
28783
|
this.stopped = false;
|
|
28156
|
-
this.
|
|
28784
|
+
const generation = ++this.generation;
|
|
28785
|
+
this.connect(generation);
|
|
28157
28786
|
}
|
|
28158
28787
|
stop() {
|
|
28159
|
-
|
|
28160
|
-
|
|
28161
|
-
|
|
28788
|
+
const socket = this.beginStop();
|
|
28789
|
+
try {
|
|
28790
|
+
socket?.close(1e3, "client shutting down");
|
|
28791
|
+
} catch {
|
|
28792
|
+
}
|
|
28793
|
+
}
|
|
28794
|
+
async stopAndWait(timeoutMs = 5e3) {
|
|
28795
|
+
const socket = this.beginStop();
|
|
28796
|
+
if (!socket || socket.readyState === wrapper_default.CLOSED) return { closeTimedOut: false };
|
|
28797
|
+
return new Promise((resolve4) => {
|
|
28798
|
+
let settled = false;
|
|
28799
|
+
const finish = (closeTimedOut) => {
|
|
28800
|
+
if (settled) return;
|
|
28801
|
+
settled = true;
|
|
28802
|
+
clearTimeout(timer);
|
|
28803
|
+
resolve4({ closeTimedOut });
|
|
28804
|
+
};
|
|
28805
|
+
const timer = setTimeout(() => {
|
|
28806
|
+
try {
|
|
28807
|
+
socket.terminate();
|
|
28808
|
+
} catch {
|
|
28809
|
+
}
|
|
28810
|
+
finish(true);
|
|
28811
|
+
}, timeoutMs);
|
|
28812
|
+
timer.unref?.();
|
|
28813
|
+
socket.once("close", () => finish(false));
|
|
28814
|
+
socket.once("error", () => finish(false));
|
|
28162
28815
|
try {
|
|
28163
|
-
|
|
28816
|
+
socket.close(1e3, "client shutting down");
|
|
28164
28817
|
} catch {
|
|
28818
|
+
finish(false);
|
|
28165
28819
|
}
|
|
28166
|
-
|
|
28167
|
-
}
|
|
28168
|
-
this.connected = false;
|
|
28169
|
-
this.authed = false;
|
|
28820
|
+
});
|
|
28170
28821
|
}
|
|
28171
28822
|
/**
|
|
28172
28823
|
* Send a reply from this agent into a conversation. Appends to the local
|
|
@@ -28176,7 +28827,7 @@ var ChatClient = class {
|
|
|
28176
28827
|
* required (the simple agent loop should just regenerate from history).
|
|
28177
28828
|
*/
|
|
28178
28829
|
async sendReply(opts) {
|
|
28179
|
-
const id =
|
|
28830
|
+
const id = randomUUID6();
|
|
28180
28831
|
const msg = {
|
|
28181
28832
|
id,
|
|
28182
28833
|
from: `agent:${this.agentName}`,
|
|
@@ -28199,27 +28850,32 @@ var ChatClient = class {
|
|
|
28199
28850
|
// -------------------------------------------------------------------------
|
|
28200
28851
|
// WebSocket lifecycle
|
|
28201
28852
|
// -------------------------------------------------------------------------
|
|
28202
|
-
connect() {
|
|
28203
|
-
if (this.
|
|
28853
|
+
connect(generation) {
|
|
28854
|
+
if (!this.isCurrent(generation)) return;
|
|
28204
28855
|
const url = this.config.url;
|
|
28205
28856
|
this.logger.info(`[perkos-chat] connecting to ${url}`);
|
|
28206
28857
|
try {
|
|
28207
28858
|
this.ws = new wrapper_default(url);
|
|
28208
28859
|
} catch (err) {
|
|
28209
28860
|
this.logger.error(`[perkos-chat] failed to construct WS: ${errMsg(err)}`);
|
|
28210
|
-
this.scheduleReconnect();
|
|
28861
|
+
this.scheduleReconnect(generation);
|
|
28211
28862
|
return;
|
|
28212
28863
|
}
|
|
28864
|
+
const socket = this.ws;
|
|
28213
28865
|
let authTimer = setTimeout(() => {
|
|
28214
|
-
if (!this.authed) {
|
|
28866
|
+
if (this.isCurrent(generation, socket) && !this.authed) {
|
|
28215
28867
|
this.logger.error("[perkos-chat] auth timeout");
|
|
28216
28868
|
try {
|
|
28217
|
-
|
|
28869
|
+
socket.close(4002, "auth timeout");
|
|
28218
28870
|
} catch {
|
|
28219
28871
|
}
|
|
28220
28872
|
}
|
|
28221
28873
|
}, AUTH_TIMEOUT_MS);
|
|
28222
28874
|
this.ws.on("open", () => {
|
|
28875
|
+
if (!this.isCurrent(generation, socket)) {
|
|
28876
|
+
socket.close(1e3, "stale chat generation");
|
|
28877
|
+
return;
|
|
28878
|
+
}
|
|
28223
28879
|
this.connected = true;
|
|
28224
28880
|
this.reconnectMs = this.config.minReconnectMs;
|
|
28225
28881
|
this.sendFrame({
|
|
@@ -28230,6 +28886,7 @@ var ChatClient = class {
|
|
|
28230
28886
|
});
|
|
28231
28887
|
});
|
|
28232
28888
|
this.ws.on("message", (data) => {
|
|
28889
|
+
if (!this.isCurrent(generation, socket)) return;
|
|
28233
28890
|
let frame;
|
|
28234
28891
|
try {
|
|
28235
28892
|
frame = JSON.parse(data.toString("utf8"));
|
|
@@ -28244,19 +28901,21 @@ var ChatClient = class {
|
|
|
28244
28901
|
void this.handleFrame(frame);
|
|
28245
28902
|
});
|
|
28246
28903
|
this.ws.on("close", (code, reason) => {
|
|
28247
|
-
this.connected = false;
|
|
28248
|
-
this.authed = false;
|
|
28249
|
-
this.clearTimers();
|
|
28250
28904
|
if (authTimer) {
|
|
28251
28905
|
clearTimeout(authTimer);
|
|
28252
28906
|
authTimer = null;
|
|
28253
28907
|
}
|
|
28908
|
+
if (!this.isCurrent(generation, socket)) return;
|
|
28909
|
+
this.connected = false;
|
|
28910
|
+
this.authed = false;
|
|
28911
|
+
this.clearTimers();
|
|
28254
28912
|
if (!this.stopped) {
|
|
28255
28913
|
this.logger.info(`[perkos-chat] disconnected (${code}: ${reason?.toString() || "no reason"}); reconnecting`);
|
|
28256
|
-
this.scheduleReconnect();
|
|
28914
|
+
this.scheduleReconnect(generation);
|
|
28257
28915
|
}
|
|
28258
28916
|
});
|
|
28259
28917
|
this.ws.on("error", (err) => {
|
|
28918
|
+
if (!this.isCurrent(generation, socket)) return;
|
|
28260
28919
|
this.logger.error(`[perkos-chat] WS error: ${errMsg(err)}`);
|
|
28261
28920
|
});
|
|
28262
28921
|
}
|
|
@@ -28445,10 +29104,12 @@ var ChatClient = class {
|
|
|
28445
29104
|
}
|
|
28446
29105
|
}, this.config.heartbeatIntervalMs);
|
|
28447
29106
|
}
|
|
28448
|
-
scheduleReconnect() {
|
|
28449
|
-
if (this.
|
|
29107
|
+
scheduleReconnect(generation) {
|
|
29108
|
+
if (!this.isCurrent(generation)) return;
|
|
28450
29109
|
const delay = this.reconnectMs;
|
|
28451
|
-
this.reconnectTimer = setTimeout(() =>
|
|
29110
|
+
this.reconnectTimer = setTimeout(() => {
|
|
29111
|
+
if (this.isCurrent(generation)) this.connect(generation);
|
|
29112
|
+
}, delay);
|
|
28452
29113
|
this.reconnectMs = Math.min(delay * 2, this.config.maxReconnectMs);
|
|
28453
29114
|
}
|
|
28454
29115
|
clearTimers() {
|
|
@@ -28461,151 +29122,25 @@ var ChatClient = class {
|
|
|
28461
29122
|
this.reconnectTimer = null;
|
|
28462
29123
|
}
|
|
28463
29124
|
}
|
|
29125
|
+
isCurrent(generation, socket) {
|
|
29126
|
+
return !this.stopped && generation === this.generation && (!socket || socket === this.ws);
|
|
29127
|
+
}
|
|
29128
|
+
beginStop() {
|
|
29129
|
+
if (this.stopped && !this.ws) return null;
|
|
29130
|
+
this.stopped = true;
|
|
29131
|
+
this.generation += 1;
|
|
29132
|
+
this.clearTimers();
|
|
29133
|
+
const socket = this.ws;
|
|
29134
|
+
this.ws = null;
|
|
29135
|
+
this.connected = false;
|
|
29136
|
+
this.authed = false;
|
|
29137
|
+
return socket;
|
|
29138
|
+
}
|
|
28464
29139
|
};
|
|
28465
29140
|
function errMsg(err) {
|
|
28466
29141
|
return err instanceof Error ? err.message : String(err);
|
|
28467
29142
|
}
|
|
28468
29143
|
|
|
28469
|
-
// src/platform-heartbeat.ts
|
|
28470
|
-
var DEFAULT_INTERVAL_MS = 6e4;
|
|
28471
|
-
var MIN_INTERVAL_MS = 1e4;
|
|
28472
|
-
function startPlatformHeartbeat(opts) {
|
|
28473
|
-
const env = opts.env ?? process.env;
|
|
28474
|
-
const fetcher = opts.fetcher ?? fetch;
|
|
28475
|
-
const url = opts.url?.trim() || env.PERKOS_HEARTBEAT_URL?.trim();
|
|
28476
|
-
const relayKey = opts.relayKey?.trim() || env.A2A_RELAY_API_KEY?.trim();
|
|
28477
|
-
if (!url || !relayKey) return { stop: () => void 0 };
|
|
28478
|
-
const configuredInterval = Number(
|
|
28479
|
-
opts.intervalMs ?? env.PERKOS_HEARTBEAT_INTERVAL_MS ?? DEFAULT_INTERVAL_MS
|
|
28480
|
-
);
|
|
28481
|
-
const intervalMs = Number.isFinite(configuredInterval) ? Math.max(MIN_INTERVAL_MS, configuredInterval) : DEFAULT_INTERVAL_MS;
|
|
28482
|
-
const a2aRuntime = env.A2A_RUNTIME?.trim() || "hermes-api";
|
|
28483
|
-
const runtimeKind = opts.runtimeKind ?? (a2aRuntime === "openclaw" ? "openclaw" : a2aRuntime === "custom" ? "custom" : "hermes");
|
|
28484
|
-
let stopped = false;
|
|
28485
|
-
let firstSuccess = true;
|
|
28486
|
-
const report = async () => {
|
|
28487
|
-
if (stopped) return;
|
|
28488
|
-
try {
|
|
28489
|
-
const res = await fetcher(url, {
|
|
28490
|
-
method: "POST",
|
|
28491
|
-
headers: {
|
|
28492
|
-
"content-type": "application/json",
|
|
28493
|
-
authorization: `Bearer ${relayKey}`,
|
|
28494
|
-
"x-relay-key": relayKey
|
|
28495
|
-
},
|
|
28496
|
-
body: JSON.stringify({
|
|
28497
|
-
runtimeKind,
|
|
28498
|
-
version: opts.version?.trim() || env.PERKOS_A2A_VERSION?.trim() || "0.12.26",
|
|
28499
|
-
ts: Date.now()
|
|
28500
|
-
})
|
|
28501
|
-
});
|
|
28502
|
-
if (res.ok) {
|
|
28503
|
-
if (firstSuccess) {
|
|
28504
|
-
opts.logger.info(
|
|
28505
|
-
`[perkos-heartbeat] reported online to ${url} (status=${res.status}, every=${intervalMs}ms)`
|
|
28506
|
-
);
|
|
28507
|
-
firstSuccess = false;
|
|
28508
|
-
}
|
|
28509
|
-
} else {
|
|
28510
|
-
opts.logger.warn(
|
|
28511
|
-
`[perkos-heartbeat] ${res.status} from ${url} (continuing)`
|
|
28512
|
-
);
|
|
28513
|
-
}
|
|
28514
|
-
} catch (err) {
|
|
28515
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
28516
|
-
opts.logger.warn(
|
|
28517
|
-
`[perkos-heartbeat] POST failed to ${url}: ${msg} (continuing)`
|
|
28518
|
-
);
|
|
28519
|
-
}
|
|
28520
|
-
};
|
|
28521
|
-
void report();
|
|
28522
|
-
const timer = setInterval(() => void report(), intervalMs);
|
|
28523
|
-
timer.unref?.();
|
|
28524
|
-
return {
|
|
28525
|
-
stop: () => {
|
|
28526
|
-
stopped = true;
|
|
28527
|
-
clearInterval(timer);
|
|
28528
|
-
}
|
|
28529
|
-
};
|
|
28530
|
-
}
|
|
28531
|
-
|
|
28532
|
-
// src/runtime-evidence.ts
|
|
28533
|
-
import { createHmac } from "node:crypto";
|
|
28534
|
-
import {
|
|
28535
|
-
chmodSync,
|
|
28536
|
-
mkdirSync,
|
|
28537
|
-
readFileSync as readFileSync2,
|
|
28538
|
-
renameSync,
|
|
28539
|
-
writeFileSync
|
|
28540
|
-
} from "node:fs";
|
|
28541
|
-
import { homedir as homedir3 } from "node:os";
|
|
28542
|
-
import { join as join2, resolve } from "node:path";
|
|
28543
|
-
var RUNTIME_EVIDENCE_SCHEMA_VERSION = 1;
|
|
28544
|
-
function canonicalize(value) {
|
|
28545
|
-
if (Array.isArray(value)) return value.map(canonicalize);
|
|
28546
|
-
if (!value || typeof value !== "object") return value;
|
|
28547
|
-
return Object.fromEntries(
|
|
28548
|
-
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, child]) => [key, canonicalize(child)])
|
|
28549
|
-
);
|
|
28550
|
-
}
|
|
28551
|
-
function canonicalConfigJson(config) {
|
|
28552
|
-
return JSON.stringify({ schemaVersion: RUNTIME_EVIDENCE_SCHEMA_VERSION, config: canonicalize(config) });
|
|
28553
|
-
}
|
|
28554
|
-
function configFingerprint(config, hmacKeyHex) {
|
|
28555
|
-
if (!/^[a-f0-9]{64}$/u.test(hmacKeyHex)) throw new Error("runtime evidence HMAC key is invalid");
|
|
28556
|
-
return createHmac("sha256", Buffer.from(hmacKeyHex, "hex")).update(canonicalConfigJson(config)).digest("hex");
|
|
28557
|
-
}
|
|
28558
|
-
function runtimeEvidenceDir(stateDir) {
|
|
28559
|
-
return resolve(stateDir ?? process.env.OPENCLAW_STATE_DIR ?? join2(homedir3(), ".openclaw"), "perkos-a2a");
|
|
28560
|
-
}
|
|
28561
|
-
function runtimeAttemptPath(stateDir) {
|
|
28562
|
-
return join2(runtimeEvidenceDir(stateDir), "active-attempt.json");
|
|
28563
|
-
}
|
|
28564
|
-
function runtimeStatusPath(stateDir) {
|
|
28565
|
-
return join2(runtimeEvidenceDir(stateDir), "runtime-status.json");
|
|
28566
|
-
}
|
|
28567
|
-
function configAgentId(config) {
|
|
28568
|
-
if (config.platform?.agentId) return config.platform.agentId;
|
|
28569
|
-
const match = config.platform?.heartbeatUrl?.match(/\/agents\/([^/]+)\/heartbeat/u);
|
|
28570
|
-
return match?.[1] ?? "unknown";
|
|
28571
|
-
}
|
|
28572
|
-
function recordRuntimeLoadEvidence(input) {
|
|
28573
|
-
const evidenceDir = runtimeEvidenceDir(input.stateDir);
|
|
28574
|
-
let attempt;
|
|
28575
|
-
try {
|
|
28576
|
-
attempt = JSON.parse(readFileSync2(runtimeAttemptPath(input.stateDir), "utf8"));
|
|
28577
|
-
} catch {
|
|
28578
|
-
input.logger.info(`[perkos-a2a] config loaded version=${input.version} agentId=${configAgentId(input.config)} attemptId=none`);
|
|
28579
|
-
return null;
|
|
28580
|
-
}
|
|
28581
|
-
const agentId = configAgentId(input.config);
|
|
28582
|
-
if (attempt.schemaVersion !== RUNTIME_EVIDENCE_SCHEMA_VERSION || attempt.targetVersion !== input.version || attempt.expectedAgentId !== agentId) {
|
|
28583
|
-
input.logger.warn?.("[perkos-a2a] runtime evidence attempt does not match the loaded plugin identity");
|
|
28584
|
-
return null;
|
|
28585
|
-
}
|
|
28586
|
-
const status = {
|
|
28587
|
-
schemaVersion: RUNTIME_EVIDENCE_SCHEMA_VERSION,
|
|
28588
|
-
loadedAt: (input.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
28589
|
-
version: input.version,
|
|
28590
|
-
agentId,
|
|
28591
|
-
fingerprint: configFingerprint(input.config, attempt.hmacKeyHex),
|
|
28592
|
-
attemptId: attempt.attemptId,
|
|
28593
|
-
pid: process.pid,
|
|
28594
|
-
pluginRoot: resolve(input.pluginRoot)
|
|
28595
|
-
};
|
|
28596
|
-
mkdirSync(evidenceDir, { recursive: true, mode: 448 });
|
|
28597
|
-
const target = runtimeStatusPath(input.stateDir);
|
|
28598
|
-
const temporary = `${target}.${process.pid}.tmp`;
|
|
28599
|
-
writeFileSync(temporary, `${JSON.stringify(status, null, 2)}
|
|
28600
|
-
`, { mode: 384 });
|
|
28601
|
-
renameSync(temporary, target);
|
|
28602
|
-
chmodSync(target, 384);
|
|
28603
|
-
input.logger.info(
|
|
28604
|
-
`[perkos-a2a] config loaded version=${status.version} agentId=${status.agentId} attemptId=${status.attemptId} fingerprint=${status.fingerprint}`
|
|
28605
|
-
);
|
|
28606
|
-
return status;
|
|
28607
|
-
}
|
|
28608
|
-
|
|
28609
29144
|
// src/types.ts
|
|
28610
29145
|
function isWebhookEvent(msg) {
|
|
28611
29146
|
return msg.type === "webhook_event";
|
|
@@ -28614,7 +29149,7 @@ function isWebhookEvent(msg) {
|
|
|
28614
29149
|
// src/index.ts
|
|
28615
29150
|
function loadedPluginVersion() {
|
|
28616
29151
|
try {
|
|
28617
|
-
const manifest = JSON.parse(
|
|
29152
|
+
const manifest = JSON.parse(readFileSync4(new URL("../openclaw.plugin.json", import.meta.url), "utf8"));
|
|
28618
29153
|
return typeof manifest.version === "string" ? manifest.version : "unknown";
|
|
28619
29154
|
} catch {
|
|
28620
29155
|
return "unknown";
|
|
@@ -28709,7 +29244,7 @@ async function prepareOpenClawChatSession(api, config, sessionKey, logger) {
|
|
|
28709
29244
|
try {
|
|
28710
29245
|
await patchSessionEntry({
|
|
28711
29246
|
sessionKey,
|
|
28712
|
-
fallbackEntry: { sessionId:
|
|
29247
|
+
fallbackEntry: { sessionId: randomUUID7(), updatedAt: Date.now() },
|
|
28713
29248
|
preserveActivity: true,
|
|
28714
29249
|
replaceEntry: true,
|
|
28715
29250
|
update: (entry) => alignOpenClawChatSessionModel(entry, config.runtime?.model)
|
|
@@ -28729,7 +29264,7 @@ async function forceNativeOpenClawChatRuntime(api, config, sessionKey, logger) {
|
|
|
28729
29264
|
try {
|
|
28730
29265
|
await patchSessionEntry({
|
|
28731
29266
|
sessionKey,
|
|
28732
|
-
fallbackEntry: { sessionId:
|
|
29267
|
+
fallbackEntry: { sessionId: randomUUID7(), updatedAt: Date.now() },
|
|
28733
29268
|
preserveActivity: true,
|
|
28734
29269
|
replaceEntry: true,
|
|
28735
29270
|
update: (entry) => ({
|
|
@@ -28770,7 +29305,7 @@ async function runOpenClawChatTurn(api, config, sessionKey, prompt, transcriptPr
|
|
|
28770
29305
|
const runtimeConfig = currentConfig();
|
|
28771
29306
|
const workspaceDir = resolveAgentWorkspaceDir(runtimeConfig, agentId);
|
|
28772
29307
|
const entry = api.runtime?.agent?.session?.getSessionEntry?.({ agentId, sessionKey });
|
|
28773
|
-
const sessionId = typeof entry?.sessionId === "string" && entry.sessionId.trim() ? entry.sessionId :
|
|
29308
|
+
const sessionId = typeof entry?.sessionId === "string" && entry.sessionId.trim() ? entry.sessionId : randomUUID7();
|
|
28774
29309
|
const configuredTimeout = api.runtime?.agent?.resolveAgentTimeoutMs?.(runtimeConfig);
|
|
28775
29310
|
const timeoutMs = Math.min(
|
|
28776
29311
|
75e3,
|
|
@@ -28785,7 +29320,7 @@ async function runOpenClawChatTurn(api, config, sessionKey, prompt, transcriptPr
|
|
|
28785
29320
|
prompt,
|
|
28786
29321
|
transcriptPrompt,
|
|
28787
29322
|
timeoutMs,
|
|
28788
|
-
runId:
|
|
29323
|
+
runId: randomUUID7(),
|
|
28789
29324
|
trigger: "user",
|
|
28790
29325
|
messageChannel: "perkos-chat",
|
|
28791
29326
|
disableTools: true,
|
|
@@ -28871,6 +29406,23 @@ async function deliverToHermes(config, message, logger) {
|
|
|
28871
29406
|
}
|
|
28872
29407
|
logger.info(`[perkos-a2a] Delivered inbound A2A task to Hermes API session ${sessionKey} via ${url}`);
|
|
28873
29408
|
}
|
|
29409
|
+
function a2aLifecycleKey(config) {
|
|
29410
|
+
const rawRelayUrl = (config.relay?.url || "no-relay").trim();
|
|
29411
|
+
let relayUrl = rawRelayUrl.replace(/[?#].*$/u, "").replace(/\/+$/u, "").toLowerCase();
|
|
29412
|
+
try {
|
|
29413
|
+
const parsed = new URL(rawRelayUrl);
|
|
29414
|
+
parsed.username = "";
|
|
29415
|
+
parsed.password = "";
|
|
29416
|
+
parsed.search = "";
|
|
29417
|
+
parsed.hash = "";
|
|
29418
|
+
relayUrl = parsed.toString().replace(/\/+$/u, "").toLowerCase();
|
|
29419
|
+
} catch {
|
|
29420
|
+
}
|
|
29421
|
+
const heartbeatUrl = config.platform?.heartbeatUrl || "";
|
|
29422
|
+
const idFromHeartbeat = heartbeatUrl.match(/\/agents\/([^/]+)\/heartbeat(?:\?|$)/u)?.[1];
|
|
29423
|
+
const agentId = config.platform?.agentId || idFromHeartbeat || config.agentName.trim().toLowerCase();
|
|
29424
|
+
return `${relayUrl}|${agentId}`;
|
|
29425
|
+
}
|
|
28874
29426
|
function register(api) {
|
|
28875
29427
|
const pluginConfig = api.config?.plugins?.entries?.["perkos-a2a"]?.config || {
|
|
28876
29428
|
agentName: "agent",
|
|
@@ -28883,10 +29435,12 @@ function register(api) {
|
|
|
28883
29435
|
recordRuntimeLoadEvidence({
|
|
28884
29436
|
config: pluginConfig,
|
|
28885
29437
|
version: pluginVersion,
|
|
28886
|
-
pluginRoot:
|
|
29438
|
+
pluginRoot: resolve3(dirname2(fileURLToPath2(import.meta.url)), ".."),
|
|
28887
29439
|
logger
|
|
28888
29440
|
});
|
|
28889
29441
|
const server = new A2AServer(pluginConfig, logger);
|
|
29442
|
+
const lifecycleKey = a2aLifecycleKey(pluginConfig);
|
|
29443
|
+
const lifecycleInstanceId = randomUUID7();
|
|
28890
29444
|
const pendingTasks = [];
|
|
28891
29445
|
const enqueueSystemEvent = api.runtime?.system?.enqueueSystemEvent;
|
|
28892
29446
|
const requestHeartbeatNow = api.runtime?.system?.requestHeartbeatNow;
|
|
@@ -28941,7 +29495,7 @@ function register(api) {
|
|
|
28941
29495
|
await deliverToHermes(pluginConfig, eventText, logger);
|
|
28942
29496
|
task.artifacts.push({
|
|
28943
29497
|
kind: "artifact",
|
|
28944
|
-
artifactId:
|
|
29498
|
+
artifactId: randomUUID7(),
|
|
28945
29499
|
parts: [{ kind: "text", text: `debug: delivered task ${task.id} to Hermes API Server` }]
|
|
28946
29500
|
});
|
|
28947
29501
|
const finalText2 = await generateRuntimeReply(pluginConfig, text, marker);
|
|
@@ -28951,14 +29505,14 @@ function register(api) {
|
|
|
28951
29505
|
if (runtimeKind === "none") {
|
|
28952
29506
|
task.artifacts.push({
|
|
28953
29507
|
kind: "artifact",
|
|
28954
|
-
artifactId:
|
|
29508
|
+
artifactId: randomUUID7(),
|
|
28955
29509
|
parts: [{ kind: "text", text: `debug: runtime delivery disabled for ${marker}` }]
|
|
28956
29510
|
});
|
|
28957
29511
|
return;
|
|
28958
29512
|
}
|
|
28959
29513
|
task.artifacts.push({
|
|
28960
29514
|
kind: "artifact",
|
|
28961
|
-
artifactId:
|
|
29515
|
+
artifactId: randomUUID7(),
|
|
28962
29516
|
parts: [{ kind: "text", text: `debug: enqueued system event for ${marker}` }]
|
|
28963
29517
|
});
|
|
28964
29518
|
if (enqueueSystemEvent) {
|
|
@@ -29042,7 +29596,7 @@ function register(api) {
|
|
|
29042
29596
|
if (!finalText) continue;
|
|
29043
29597
|
task.artifacts.push({
|
|
29044
29598
|
kind: "artifact",
|
|
29045
|
-
artifactId:
|
|
29599
|
+
artifactId: randomUUID7(),
|
|
29046
29600
|
parts: [{ kind: "text", text: finalText }]
|
|
29047
29601
|
});
|
|
29048
29602
|
task.status = {
|
|
@@ -29056,169 +29610,137 @@ function register(api) {
|
|
|
29056
29610
|
logger.info(`[perkos-a2a] Task ${task.id} completed from transcript update`);
|
|
29057
29611
|
}
|
|
29058
29612
|
});
|
|
29059
|
-
api.registerService({
|
|
29060
|
-
id: "perkos-a2a",
|
|
29061
|
-
start: () => {
|
|
29062
|
-
server.start();
|
|
29063
|
-
logger.info(`[perkos-a2a] A2A server started for ${pluginConfig.agentName}`);
|
|
29064
|
-
},
|
|
29065
|
-
stop: () => {
|
|
29066
|
-
logger.info("[perkos-a2a] A2A server stopping");
|
|
29067
|
-
}
|
|
29068
|
-
});
|
|
29069
29613
|
const chatApiKey = pluginConfig.chat?.apiKey || pluginConfig.relay?.apiKey;
|
|
29070
|
-
const
|
|
29071
|
-
|
|
29072
|
-
|
|
29073
|
-
|
|
29074
|
-
|
|
29075
|
-
|
|
29076
|
-
|
|
29077
|
-
|
|
29078
|
-
|
|
29079
|
-
|
|
29080
|
-
|
|
29081
|
-
|
|
29082
|
-
|
|
29083
|
-
|
|
29084
|
-
|
|
29085
|
-
|
|
29086
|
-
|
|
29087
|
-
|
|
29088
|
-
|
|
29614
|
+
const chatEnabled = Boolean(pluginConfig.chat?.enabled && chatApiKey);
|
|
29615
|
+
const chatStore = chatEnabled ? new ChatStore({ storeRoot: pluginConfig.chat?.storeRoot }) : null;
|
|
29616
|
+
let supervisorClient;
|
|
29617
|
+
const handleSupervisorChatDeliver = async (frame) => {
|
|
29618
|
+
const walletAddress = parseWalletFromIdentity(frame.from);
|
|
29619
|
+
const marker = `[PERKOS_CHAT:${frame.convId}]`;
|
|
29620
|
+
const recentPage = await chatStore?.readPage(frame.convId, {
|
|
29621
|
+
limit: CHAT_CONTEXT_MESSAGE_LIMIT + 1
|
|
29622
|
+
});
|
|
29623
|
+
const recentContext = formatRecentChatContext(recentPage?.messages ?? [], frame.id);
|
|
29624
|
+
const eventText = [
|
|
29625
|
+
marker,
|
|
29626
|
+
`From: ${frame.from}`,
|
|
29627
|
+
`Conversation: ${frame.convId}`,
|
|
29628
|
+
frame.projectId ? `Canonical projectId: ${frame.projectId}` : "",
|
|
29629
|
+
walletAddress ? `Wallet: ${walletAddress}` : "",
|
|
29630
|
+
"",
|
|
29631
|
+
"A user is messaging you in a PerkOS chat conversation.",
|
|
29632
|
+
"To respond, call the `perkos_chat_reply` tool with:",
|
|
29633
|
+
` convId: "${frame.convId}"`,
|
|
29634
|
+
walletAddress ? ` walletAddress: "${walletAddress}"` : ` walletAddress: <derived from From>`,
|
|
29635
|
+
` text: <your reply>`,
|
|
29636
|
+
"",
|
|
29637
|
+
"Recent conversation context (oldest to newest):",
|
|
29638
|
+
recentContext,
|
|
29639
|
+
"",
|
|
29640
|
+
"Use the recent context. Do not ask again for information the user already provided.",
|
|
29641
|
+
"",
|
|
29642
|
+
"Message body:",
|
|
29643
|
+
frame.text
|
|
29644
|
+
].filter(Boolean).join("\n");
|
|
29645
|
+
const directRunPrompt = [
|
|
29646
|
+
marker,
|
|
29647
|
+
`From: ${frame.from}`,
|
|
29648
|
+
`Conversation: ${frame.convId}`,
|
|
29649
|
+
frame.projectId ? `Canonical projectId: ${frame.projectId}` : "",
|
|
29650
|
+
"",
|
|
29651
|
+
"Reply to this PerkOS chat message using your current runtime configuration.",
|
|
29652
|
+
"Return only the reply text. Do not call messaging tools; the PerkOS plugin delivers the returned text.",
|
|
29653
|
+
"Use the recent context and do not ask again for information already provided.",
|
|
29654
|
+
"",
|
|
29655
|
+
"Recent conversation context (oldest to newest):",
|
|
29656
|
+
recentContext,
|
|
29657
|
+
"",
|
|
29658
|
+
"Message body:",
|
|
29659
|
+
frame.text
|
|
29660
|
+
].filter(Boolean).join("\n");
|
|
29661
|
+
const runtimeKind = pluginConfig.runtime?.kind || "openclaw";
|
|
29662
|
+
if (runtimeKind === "hermes" || runtimeKind === "hermes-api") {
|
|
29663
|
+
try {
|
|
29664
|
+
await deliverToHermes(pluginConfig, eventText, logger);
|
|
29665
|
+
} catch (err) {
|
|
29666
|
+
logger.error(`[perkos-chat] hermes delivery failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
29667
|
+
}
|
|
29668
|
+
return;
|
|
29669
|
+
}
|
|
29670
|
+
if (runtimeKind === "none") {
|
|
29671
|
+
logger.info(`[perkos-chat] dropped chat_deliver for ${frame.convId} (runtime=none)`);
|
|
29672
|
+
return;
|
|
29673
|
+
}
|
|
29674
|
+
const chatSessionKey = openclawChatSessionKey(pluginConfig, frame.convId);
|
|
29675
|
+
await prepareOpenClawChatSession(api, pluginConfig, chatSessionKey, logger);
|
|
29676
|
+
if (walletAddress) {
|
|
29677
|
+
const reply = await runOpenClawChatTurn(api, pluginConfig, chatSessionKey, directRunPrompt, frame.text, logger);
|
|
29678
|
+
if (reply) {
|
|
29679
|
+
const result = await supervisorClient.sendChatReply({
|
|
29680
|
+
convId: frame.convId,
|
|
29681
|
+
walletAddress,
|
|
29682
|
+
text: reply,
|
|
29683
|
+
replyTo: frame.id
|
|
29089
29684
|
});
|
|
29090
|
-
|
|
29091
|
-
|
|
29092
|
-
frame.id
|
|
29093
|
-
);
|
|
29094
|
-
const eventText = [
|
|
29095
|
-
marker,
|
|
29096
|
-
`From: ${frame.from}`,
|
|
29097
|
-
`Conversation: ${frame.convId}`,
|
|
29098
|
-
frame.projectId ? `Canonical projectId: ${frame.projectId}` : "",
|
|
29099
|
-
walletAddress ? `Wallet: ${walletAddress}` : "",
|
|
29100
|
-
"",
|
|
29101
|
-
"A user is messaging you in a PerkOS chat conversation.",
|
|
29102
|
-
"To respond, call the `perkos_chat_reply` tool with:",
|
|
29103
|
-
` convId: "${frame.convId}"`,
|
|
29104
|
-
walletAddress ? ` walletAddress: "${walletAddress}"` : ` walletAddress: <derived from From>`,
|
|
29105
|
-
` text: <your reply>`,
|
|
29106
|
-
"",
|
|
29107
|
-
"Recent conversation context (oldest to newest):",
|
|
29108
|
-
recentContext,
|
|
29109
|
-
"",
|
|
29110
|
-
"Use the recent context. Do not ask again for information the user already provided.",
|
|
29111
|
-
"",
|
|
29112
|
-
"Message body:",
|
|
29113
|
-
frame.text
|
|
29114
|
-
].filter(Boolean).join("\n");
|
|
29115
|
-
const directRunPrompt = [
|
|
29116
|
-
marker,
|
|
29117
|
-
`From: ${frame.from}`,
|
|
29118
|
-
`Conversation: ${frame.convId}`,
|
|
29119
|
-
frame.projectId ? `Canonical projectId: ${frame.projectId}` : "",
|
|
29120
|
-
"",
|
|
29121
|
-
"Reply to this PerkOS chat message using your current runtime configuration.",
|
|
29122
|
-
"Return only the reply text. Do not call messaging tools; the PerkOS plugin delivers the returned text.",
|
|
29123
|
-
"Use the recent context and do not ask again for information already provided.",
|
|
29124
|
-
"",
|
|
29125
|
-
"Recent conversation context (oldest to newest):",
|
|
29126
|
-
recentContext,
|
|
29127
|
-
"",
|
|
29128
|
-
"Message body:",
|
|
29129
|
-
frame.text
|
|
29130
|
-
].filter(Boolean).join("\n");
|
|
29131
|
-
const runtimeKind = pluginConfig.runtime?.kind || "openclaw";
|
|
29132
|
-
if (runtimeKind === "hermes" || runtimeKind === "hermes-api") {
|
|
29133
|
-
try {
|
|
29134
|
-
await deliverToHermes(pluginConfig, eventText, logger);
|
|
29135
|
-
} catch (err) {
|
|
29136
|
-
logger.error(`[perkos-chat] hermes delivery failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
29137
|
-
}
|
|
29138
|
-
return;
|
|
29139
|
-
}
|
|
29140
|
-
if (runtimeKind === "none") {
|
|
29141
|
-
logger.info(`[perkos-chat] dropped chat_deliver for ${frame.convId} (runtime=none)`);
|
|
29142
|
-
return;
|
|
29143
|
-
}
|
|
29144
|
-
const chatSessionKey = openclawChatSessionKey(pluginConfig, frame.convId);
|
|
29145
|
-
await prepareOpenClawChatSession(api, pluginConfig, chatSessionKey, logger);
|
|
29146
|
-
if (walletAddress) {
|
|
29147
|
-
const reply = await runOpenClawChatTurn(
|
|
29148
|
-
api,
|
|
29149
|
-
pluginConfig,
|
|
29150
|
-
chatSessionKey,
|
|
29151
|
-
directRunPrompt,
|
|
29152
|
-
frame.text,
|
|
29153
|
-
logger
|
|
29154
|
-
);
|
|
29155
|
-
if (reply) {
|
|
29156
|
-
const result = await chatClient?.sendReply({
|
|
29157
|
-
convId: frame.convId,
|
|
29158
|
-
walletAddress,
|
|
29159
|
-
text: reply,
|
|
29160
|
-
replyTo: frame.id
|
|
29161
|
-
});
|
|
29162
|
-
logger.info(
|
|
29163
|
-
`[perkos-chat] embedded reply ${result?.delivered ? "delivered" : "queued (offline)"} for ${frame.convId}`
|
|
29164
|
-
);
|
|
29165
|
-
return;
|
|
29166
|
-
}
|
|
29167
|
-
}
|
|
29168
|
-
if (enqueueSystemEvent) {
|
|
29169
|
-
enqueueSystemEvent(eventText, { sessionKey: chatSessionKey });
|
|
29170
|
-
}
|
|
29171
|
-
wakeGatewayAgent(
|
|
29172
|
-
requestHeartbeatNow,
|
|
29173
|
-
`[PerkOS-Chat] inbound message in ${frame.convId}`,
|
|
29174
|
-
logger,
|
|
29175
|
-
chatSessionKey
|
|
29176
|
-
);
|
|
29177
|
-
},
|
|
29178
|
-
onChannelJoin: async (frame) => {
|
|
29179
|
-
logger.info(`[perkos-chat] joined conv ${frame.convId} (host: ${frame.historyHost}, ${frame.participants.length} participants)`);
|
|
29685
|
+
logger.info(`[perkos-chat] embedded reply ${result.delivered ? "delivered" : "queued (offline)"} for ${frame.convId}`);
|
|
29686
|
+
return;
|
|
29180
29687
|
}
|
|
29181
29688
|
}
|
|
29182
|
-
|
|
29689
|
+
if (enqueueSystemEvent) enqueueSystemEvent(eventText, { sessionKey: chatSessionKey });
|
|
29690
|
+
wakeGatewayAgent(requestHeartbeatNow, `[PerkOS-Chat] inbound message in ${frame.convId}`, logger, chatSessionKey);
|
|
29691
|
+
};
|
|
29183
29692
|
if (pluginConfig.chat?.enabled && !chatApiKey) {
|
|
29184
29693
|
logger.error("[perkos-chat] chat.enabled=true but no apiKey (set chat.apiKey or fall back from relay.apiKey)");
|
|
29185
29694
|
}
|
|
29186
|
-
|
|
29187
|
-
|
|
29188
|
-
|
|
29189
|
-
|
|
29190
|
-
|
|
29191
|
-
|
|
29192
|
-
|
|
29193
|
-
|
|
29194
|
-
|
|
29195
|
-
|
|
29196
|
-
|
|
29197
|
-
|
|
29198
|
-
|
|
29199
|
-
|
|
29200
|
-
|
|
29201
|
-
|
|
29202
|
-
|
|
29203
|
-
|
|
29204
|
-
|
|
29205
|
-
|
|
29206
|
-
|
|
29207
|
-
|
|
29208
|
-
|
|
29209
|
-
|
|
29210
|
-
|
|
29211
|
-
|
|
29212
|
-
|
|
29213
|
-
|
|
29214
|
-
|
|
29215
|
-
|
|
29216
|
-
|
|
29217
|
-
|
|
29218
|
-
platformHeartbeat = null;
|
|
29695
|
+
const platformAgentId = pluginConfig.platform?.agentId || pluginConfig.platform?.heartbeatUrl?.match(/\/agents\/([^/]+)\/heartbeat/u)?.[1] || pluginConfig.agentName.trim().toLowerCase();
|
|
29696
|
+
supervisorClient = new OpenClawSupervisorClient({
|
|
29697
|
+
ownerHash: supervisorOwnerHash(lifecycleKey),
|
|
29698
|
+
generation: Date.now(),
|
|
29699
|
+
instanceId: lifecycleInstanceId,
|
|
29700
|
+
config: {
|
|
29701
|
+
agentName: pluginConfig.agentName,
|
|
29702
|
+
agentId: platformAgentId,
|
|
29703
|
+
version: pluginVersion,
|
|
29704
|
+
relay: pluginConfig.relay,
|
|
29705
|
+
chat: chatEnabled ? {
|
|
29706
|
+
enabled: true,
|
|
29707
|
+
url: pluginConfig.chat?.url || "wss://chat.perkos.xyz/chat",
|
|
29708
|
+
apiKey: chatApiKey || "",
|
|
29709
|
+
storeRoot: pluginConfig.chat?.storeRoot,
|
|
29710
|
+
defaultHistoryLimit: pluginConfig.chat?.defaultHistoryLimit,
|
|
29711
|
+
minReconnectMs: pluginConfig.chat?.minReconnectMs,
|
|
29712
|
+
maxReconnectMs: pluginConfig.chat?.maxReconnectMs,
|
|
29713
|
+
heartbeatIntervalMs: pluginConfig.chat?.heartbeatIntervalMs
|
|
29714
|
+
} : void 0,
|
|
29715
|
+
platform: {
|
|
29716
|
+
heartbeatUrl: pluginConfig.platform?.heartbeatUrl,
|
|
29717
|
+
heartbeatIntervalMs: pluginConfig.platform?.heartbeatIntervalMs
|
|
29718
|
+
},
|
|
29719
|
+
agentCard: server.getAgentCard()
|
|
29720
|
+
},
|
|
29721
|
+
logger,
|
|
29722
|
+
handlers: {
|
|
29723
|
+
onRelayTask: (message) => server.handleTransportTask(message),
|
|
29724
|
+
onChatDeliver: handleSupervisorChatDeliver,
|
|
29725
|
+
onChannelJoin: async (frame) => {
|
|
29726
|
+
logger.info(`[perkos-chat] joined conv ${frame.convId} (host: ${frame.historyHost}, ${frame.participants.length} participants)`);
|
|
29219
29727
|
}
|
|
29220
|
-
}
|
|
29221
|
-
}
|
|
29728
|
+
}
|
|
29729
|
+
});
|
|
29730
|
+
server.setRelayTransport(supervisorClient);
|
|
29731
|
+
api.registerService({
|
|
29732
|
+
id: "perkos-a2a",
|
|
29733
|
+
start: async () => {
|
|
29734
|
+
await supervisorClient.start();
|
|
29735
|
+
await server.start();
|
|
29736
|
+
logger.info(`[perkos-a2a] A2A server started for ${pluginConfig.agentName}`);
|
|
29737
|
+
},
|
|
29738
|
+
stop: async () => {
|
|
29739
|
+
logger.info("[perkos-a2a] A2A server stopping");
|
|
29740
|
+
await server.stop({ reason: "openclaw-service-stop", timeoutMs: 8e3, drainMs: 3e3 });
|
|
29741
|
+
await supervisorClient.stop();
|
|
29742
|
+
}
|
|
29743
|
+
});
|
|
29222
29744
|
api.registerTool({
|
|
29223
29745
|
name: "perkos_a2a_send",
|
|
29224
29746
|
description: "Send a task to another agent via PerkOS A2A. Use this to delegate work, ask a peer to research, or route a chain/circular workflow. The message may name additional agents and a final notification target; the bridge preserves the route and prevents loops.",
|
|
@@ -29405,13 +29927,13 @@ function register(api) {
|
|
|
29405
29927
|
required: ["convId", "walletAddress", "text"]
|
|
29406
29928
|
},
|
|
29407
29929
|
async execute(_id, params) {
|
|
29408
|
-
if (!
|
|
29930
|
+
if (!chatEnabled) {
|
|
29409
29931
|
return {
|
|
29410
29932
|
content: [{ type: "text", text: "PerkOS-Chat client is not enabled in this agent's config." }]
|
|
29411
29933
|
};
|
|
29412
29934
|
}
|
|
29413
29935
|
try {
|
|
29414
|
-
const result = await
|
|
29936
|
+
const result = await supervisorClient.sendChatReply({
|
|
29415
29937
|
convId: params.convId,
|
|
29416
29938
|
walletAddress: params.walletAddress,
|
|
29417
29939
|
text: params.text,
|
|
@@ -29452,14 +29974,13 @@ function register(api) {
|
|
|
29452
29974
|
required: ["convId"]
|
|
29453
29975
|
},
|
|
29454
29976
|
async execute(_id, params) {
|
|
29455
|
-
if (!
|
|
29977
|
+
if (!chatStore) {
|
|
29456
29978
|
return {
|
|
29457
29979
|
content: [{ type: "text", text: "PerkOS-Chat client is not enabled in this agent's config." }]
|
|
29458
29980
|
};
|
|
29459
29981
|
}
|
|
29460
29982
|
try {
|
|
29461
|
-
const
|
|
29462
|
-
const page = await store.readPage(params.convId, {
|
|
29983
|
+
const page = await chatStore.readPage(params.convId, {
|
|
29463
29984
|
before: params.before ?? null,
|
|
29464
29985
|
limit: params.limit ?? 50
|
|
29465
29986
|
});
|
|
@@ -29488,9 +30009,9 @@ function register(api) {
|
|
|
29488
30009
|
hasSystemEventInjection: !!enqueueSystemEvent,
|
|
29489
30010
|
protocol: "a2a",
|
|
29490
30011
|
version: "0.8.4",
|
|
29491
|
-
chat:
|
|
30012
|
+
chat: chatEnabled ? {
|
|
29492
30013
|
enabled: true,
|
|
29493
|
-
connected:
|
|
30014
|
+
connected: supervisorClient.isChatConnected(),
|
|
29494
30015
|
url: pluginConfig.chat?.url || "wss://chat.perkos.xyz/chat"
|
|
29495
30016
|
} : { enabled: false, connected: false, url: null }
|
|
29496
30017
|
});
|
|
@@ -29508,8 +30029,8 @@ function register(api) {
|
|
|
29508
30029
|
console.log(`Auth required: ${pluginConfig.auth?.requireApiKey || false}`);
|
|
29509
30030
|
console.log(`Peers: ${Object.keys(pluginConfig.peers).join(", ") || "(none)"}`);
|
|
29510
30031
|
console.log(`System event injection: ${!!enqueueSystemEvent}`);
|
|
29511
|
-
if (
|
|
29512
|
-
console.log(`Chat: enabled (connected=${
|
|
30032
|
+
if (chatEnabled) {
|
|
30033
|
+
console.log(`Chat: enabled (connected=${supervisorClient.isChatConnected()}, url=${pluginConfig.chat?.url || "wss://chat.perkos.xyz/chat"})`);
|
|
29513
30034
|
} else {
|
|
29514
30035
|
console.log("Chat: disabled");
|
|
29515
30036
|
}
|
|
@@ -29530,26 +30051,26 @@ function register(api) {
|
|
|
29530
30051
|
console.log("[perkos-a2a] Detecting environment...\n");
|
|
29531
30052
|
const isMac = process.platform === "darwin";
|
|
29532
30053
|
console.log(`Platform: ${process.platform}${isMac ? " (macOS)" : ""}`);
|
|
29533
|
-
const
|
|
29534
|
-
console.log(`Public IP: ${
|
|
29535
|
-
console.log(`Local IPs: ${
|
|
29536
|
-
console.log(`Behind NAT: ${
|
|
29537
|
-
console.log(`Tailscale: ${
|
|
30054
|
+
const net2 = await detectNetworking();
|
|
30055
|
+
console.log(`Public IP: ${net2.publicIp || "unknown"}`);
|
|
30056
|
+
console.log(`Local IPs: ${net2.localIps.join(", ") || "none"}`);
|
|
30057
|
+
console.log(`Behind NAT: ${net2.isBehindNat ? "yes" : "no"}`);
|
|
30058
|
+
console.log(`Tailscale: ${net2.hasTailscale ? "installed" : "not found"}${net2.tailscaleIp ? ` (${net2.tailscaleIp})` : ""}`);
|
|
29538
30059
|
let portAvailable = true;
|
|
29539
30060
|
try {
|
|
29540
30061
|
const netMod = await import("net");
|
|
29541
|
-
await new Promise((
|
|
30062
|
+
await new Promise((resolve4, reject) => {
|
|
29542
30063
|
const srv = netMod.createServer();
|
|
29543
30064
|
srv.once("error", (err) => {
|
|
29544
30065
|
if (err.code === "EADDRINUSE") {
|
|
29545
30066
|
portAvailable = false;
|
|
29546
|
-
|
|
30067
|
+
resolve4();
|
|
29547
30068
|
} else {
|
|
29548
30069
|
reject(err);
|
|
29549
30070
|
}
|
|
29550
30071
|
});
|
|
29551
30072
|
srv.listen(pluginConfig.port, () => {
|
|
29552
|
-
srv.close(() =>
|
|
30073
|
+
srv.close(() => resolve4());
|
|
29553
30074
|
});
|
|
29554
30075
|
});
|
|
29555
30076
|
} catch {
|
|
@@ -29569,14 +30090,14 @@ Relay: ${pluginConfig.relay?.enabled ? "enabled" : "disabled"}`);
|
|
|
29569
30090
|
console.log(` Local loopback URL for this agent: http://127.0.0.1:${pluginConfig.port}/a2a/jsonrpc`);
|
|
29570
30091
|
console.log(" Configure same-host peers as http://127.0.0.1:<peer-port>, not by shared public IP.");
|
|
29571
30092
|
console.log("");
|
|
29572
|
-
if (!
|
|
30093
|
+
if (!net2.isBehindNat) {
|
|
29573
30094
|
console.log("You have a public IP. Use it only for remote peers that cannot reach a local/tunnel URL.");
|
|
29574
|
-
console.log(` Public A2A URL candidate: http://${
|
|
30095
|
+
console.log(` Public A2A URL candidate: http://${net2.publicIp}:${pluginConfig.port}/a2a/jsonrpc`);
|
|
29575
30096
|
} else if (pluginConfig.relay?.enabled) {
|
|
29576
30097
|
console.log("You are behind NAT but relay is configured. Bidirectional communication via relay.");
|
|
29577
|
-
} else if (
|
|
30098
|
+
} else if (net2.hasTailscale && net2.tailscaleIp) {
|
|
29578
30099
|
console.log("Use your Tailscale IP for peers.");
|
|
29579
|
-
console.log(` Your A2A URL: http://${
|
|
30100
|
+
console.log(` Your A2A URL: http://${net2.tailscaleIp}:${pluginConfig.port}/a2a/jsonrpc`);
|
|
29580
30101
|
} else {
|
|
29581
30102
|
console.log("You are behind NAT. Options:");
|
|
29582
30103
|
console.log(" 1) Configure a relay hub (recommended) - set relay.url and relay.enabled in config");
|
|
@@ -29600,6 +30121,7 @@ export {
|
|
|
29600
30121
|
ChatStore,
|
|
29601
30122
|
RelayClient,
|
|
29602
30123
|
RelayHub,
|
|
30124
|
+
a2aLifecycleKey,
|
|
29603
30125
|
alignOpenClawChatSessionModel,
|
|
29604
30126
|
register as default,
|
|
29605
30127
|
detectNetworking,
|