@perkos/perkos-a2a 0.12.31 → 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.map +1 -1
- package/dist/index.js +702 -421
- 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/repair.d.ts +3 -1
- package/dist/repair.d.ts.map +1 -1
- package/dist/repair.js +42 -35
- 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 +17 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +41 -6
- 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",
|
|
@@ -26291,13 +26291,13 @@ var RelayClient = class {
|
|
|
26291
26291
|
async stopAndWait(timeoutMs = 5e3) {
|
|
26292
26292
|
const socket = this.beginStop();
|
|
26293
26293
|
if (!socket || socket.readyState === wrapper_default.CLOSED) return { closeTimedOut: false };
|
|
26294
|
-
return new Promise((
|
|
26294
|
+
return new Promise((resolve4) => {
|
|
26295
26295
|
let settled = false;
|
|
26296
26296
|
const finish = (closeTimedOut) => {
|
|
26297
26297
|
if (settled) return;
|
|
26298
26298
|
settled = true;
|
|
26299
26299
|
clearTimeout(timer);
|
|
26300
|
-
|
|
26300
|
+
resolve4({ closeTimedOut });
|
|
26301
26301
|
};
|
|
26302
26302
|
const timer = setTimeout(() => {
|
|
26303
26303
|
finish(true);
|
|
@@ -26488,7 +26488,7 @@ var RelayClient = class {
|
|
|
26488
26488
|
}
|
|
26489
26489
|
}
|
|
26490
26490
|
async sendAndWait(msg, expectedType, timeoutMs) {
|
|
26491
|
-
return new Promise((
|
|
26491
|
+
return new Promise((resolve4, reject) => {
|
|
26492
26492
|
const timer = setTimeout(() => {
|
|
26493
26493
|
this.pendingCallbacks.delete(msg.id);
|
|
26494
26494
|
reject(new Error(`Relay request timed out after ${timeoutMs}ms`));
|
|
@@ -26500,7 +26500,7 @@ var RelayClient = class {
|
|
|
26500
26500
|
if (response.type === "error") {
|
|
26501
26501
|
reject(new Error(`Relay error: ${response.payload.message}`));
|
|
26502
26502
|
} else {
|
|
26503
|
-
|
|
26503
|
+
resolve4(response);
|
|
26504
26504
|
}
|
|
26505
26505
|
},
|
|
26506
26506
|
cancel: (error) => {
|
|
@@ -26603,13 +26603,13 @@ var RelayHub = class {
|
|
|
26603
26603
|
for (const agent of this.agents.values()) {
|
|
26604
26604
|
agent.ws.close(1001, "Relay shutting down");
|
|
26605
26605
|
}
|
|
26606
|
-
await new Promise((
|
|
26606
|
+
await new Promise((resolve4) => {
|
|
26607
26607
|
let settled = false;
|
|
26608
26608
|
const done = () => {
|
|
26609
26609
|
if (settled) return;
|
|
26610
26610
|
settled = true;
|
|
26611
26611
|
clearTimeout(timer);
|
|
26612
|
-
|
|
26612
|
+
resolve4();
|
|
26613
26613
|
};
|
|
26614
26614
|
const timer = setTimeout(() => {
|
|
26615
26615
|
for (const client of wss.clients) client.terminate();
|
|
@@ -27008,6 +27008,7 @@ var A2AServer = class {
|
|
|
27008
27008
|
logger;
|
|
27009
27009
|
clientOnly = false;
|
|
27010
27010
|
relayClient = null;
|
|
27011
|
+
externalRelayTransport = false;
|
|
27011
27012
|
relayHub = null;
|
|
27012
27013
|
messageInjector = null;
|
|
27013
27014
|
taskResultHandler = null;
|
|
@@ -27047,6 +27048,14 @@ var A2AServer = class {
|
|
|
27047
27048
|
setTaskFailureHandler(handler) {
|
|
27048
27049
|
this.taskFailureHandler = handler;
|
|
27049
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
|
+
}
|
|
27050
27059
|
getTasks() {
|
|
27051
27060
|
return this.tasks.values();
|
|
27052
27061
|
}
|
|
@@ -27062,6 +27071,9 @@ var A2AServer = class {
|
|
|
27062
27071
|
isRelayConnected() {
|
|
27063
27072
|
return this.relayClient?.isConnected() || false;
|
|
27064
27073
|
}
|
|
27074
|
+
getAgentCard() {
|
|
27075
|
+
return structuredClone(this.agentCard);
|
|
27076
|
+
}
|
|
27065
27077
|
authMiddleware() {
|
|
27066
27078
|
return (req, res, next) => {
|
|
27067
27079
|
if (!this.config.auth?.requireApiKey) {
|
|
@@ -27324,7 +27336,7 @@ var A2AServer = class {
|
|
|
27324
27336
|
async waitForRemoteTaskTerminal(targetUrl, headers, taskId, timeoutMs = 45e3, pollMs = 1e3) {
|
|
27325
27337
|
const deadline = Date.now() + timeoutMs;
|
|
27326
27338
|
while (Date.now() < deadline) {
|
|
27327
|
-
await new Promise((
|
|
27339
|
+
await new Promise((resolve4) => setTimeout(resolve4, pollMs));
|
|
27328
27340
|
const response = await fetch(`${targetUrl}/a2a/jsonrpc`, {
|
|
27329
27341
|
method: "POST",
|
|
27330
27342
|
headers,
|
|
@@ -27348,7 +27360,7 @@ var A2AServer = class {
|
|
|
27348
27360
|
if (task && isTerminalTaskState(task.status?.state)) {
|
|
27349
27361
|
return this.success(randomUUID4(), structuredClone(task));
|
|
27350
27362
|
}
|
|
27351
|
-
await new Promise((
|
|
27363
|
+
await new Promise((resolve4) => setTimeout(resolve4, pollMs));
|
|
27352
27364
|
}
|
|
27353
27365
|
return null;
|
|
27354
27366
|
}
|
|
@@ -27454,9 +27466,30 @@ var A2AServer = class {
|
|
|
27454
27466
|
});
|
|
27455
27467
|
this.trackOperation(operation);
|
|
27456
27468
|
}
|
|
27457
|
-
|
|
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() {
|
|
27458
27486
|
const relay = this.config.relay;
|
|
27459
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
|
+
}
|
|
27460
27493
|
this.relayClient = new RelayClient({
|
|
27461
27494
|
agentName: this.config.agentName,
|
|
27462
27495
|
relay,
|
|
@@ -27509,7 +27542,7 @@ var A2AServer = class {
|
|
|
27509
27542
|
if (mode === "client-only") {
|
|
27510
27543
|
if (!this.isGenerationActive(generation, "starting")) return;
|
|
27511
27544
|
this.clientOnly = true;
|
|
27512
|
-
this.startRelayClient();
|
|
27545
|
+
await this.startRelayClient();
|
|
27513
27546
|
if (!this.config.relay?.enabled) {
|
|
27514
27547
|
this.logger.info(
|
|
27515
27548
|
"[perkos-a2a] Running in client-only mode. Configure relay for NAT traversal or set up Tailscale/tunnel."
|
|
@@ -27520,19 +27553,19 @@ var A2AServer = class {
|
|
|
27520
27553
|
if (mode === "full") {
|
|
27521
27554
|
await this.tryListen(this.config.port, 1, generation);
|
|
27522
27555
|
if (!this.isGenerationActive(generation, "starting")) return;
|
|
27523
|
-
this.startRelayClient();
|
|
27556
|
+
await this.startRelayClient();
|
|
27524
27557
|
return;
|
|
27525
27558
|
}
|
|
27526
27559
|
try {
|
|
27527
|
-
const
|
|
27560
|
+
const net2 = await detectNetworking();
|
|
27528
27561
|
if (!this.isGenerationActive(generation, "starting")) return;
|
|
27529
|
-
if (
|
|
27562
|
+
if (net2.isBehindNat && !net2.hasTailscale) {
|
|
27530
27563
|
if (this.config.relay?.enabled) {
|
|
27531
27564
|
this.logger.info(
|
|
27532
27565
|
"[perkos-a2a] Behind NAT, using relay for bidirectional communication"
|
|
27533
27566
|
);
|
|
27534
27567
|
this.clientOnly = true;
|
|
27535
|
-
this.startRelayClient();
|
|
27568
|
+
await this.startRelayClient();
|
|
27536
27569
|
} else {
|
|
27537
27570
|
this.logger.info(
|
|
27538
27571
|
"[perkos-a2a] Running in client-only mode (behind NAT). Configure relay or Tailscale for bidirectional A2A."
|
|
@@ -27541,16 +27574,16 @@ var A2AServer = class {
|
|
|
27541
27574
|
}
|
|
27542
27575
|
return;
|
|
27543
27576
|
}
|
|
27544
|
-
if (
|
|
27577
|
+
if (net2.isBehindNat && net2.hasTailscale && net2.tailscaleIp) {
|
|
27545
27578
|
this.logger.info(
|
|
27546
|
-
`[perkos-a2a] Behind NAT but Tailscale detected (${
|
|
27579
|
+
`[perkos-a2a] Behind NAT but Tailscale detected (${net2.tailscaleIp}). Starting server.`
|
|
27547
27580
|
);
|
|
27548
27581
|
}
|
|
27549
27582
|
} catch {
|
|
27550
27583
|
}
|
|
27551
27584
|
await this.tryListen(this.config.port, 1, generation);
|
|
27552
27585
|
if (!this.isGenerationActive(generation, "starting")) return;
|
|
27553
|
-
this.startRelayClient();
|
|
27586
|
+
await this.startRelayClient();
|
|
27554
27587
|
}
|
|
27555
27588
|
stop(options = {}) {
|
|
27556
27589
|
if (this.lifecycleState === "stopping" && this.stopPromise) return this.stopPromise;
|
|
@@ -27648,7 +27681,7 @@ var A2AServer = class {
|
|
|
27648
27681
|
const deadline = Date.now() + timeoutMs;
|
|
27649
27682
|
const remaining = () => Math.max(1, deadline - Date.now());
|
|
27650
27683
|
const relayClient = this.relayClient;
|
|
27651
|
-
this.relayClient = null;
|
|
27684
|
+
if (!this.externalRelayTransport) this.relayClient = null;
|
|
27652
27685
|
await relayClient?.stopAndWait(remaining());
|
|
27653
27686
|
const relayHub = this.relayHub;
|
|
27654
27687
|
this.relayHub = null;
|
|
@@ -27678,10 +27711,10 @@ var A2AServer = class {
|
|
|
27678
27711
|
};
|
|
27679
27712
|
|
|
27680
27713
|
// src/index.ts
|
|
27681
|
-
import { randomUUID as
|
|
27682
|
-
import { readFileSync as
|
|
27683
|
-
import { dirname, resolve as
|
|
27684
|
-
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";
|
|
27685
27718
|
|
|
27686
27719
|
// src/agentic-actions.ts
|
|
27687
27720
|
var FALLBACK_AGENT_ALIASES = [
|
|
@@ -28096,9 +28129,6 @@ ${replyResultText2}` : void 0,
|
|
|
28096
28129
|
return true;
|
|
28097
28130
|
}
|
|
28098
28131
|
|
|
28099
|
-
// src/chat-client.ts
|
|
28100
|
-
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
28101
|
-
|
|
28102
28132
|
// src/chat-store.ts
|
|
28103
28133
|
import { createHash } from "node:crypto";
|
|
28104
28134
|
import { createReadStream } from "node:fs";
|
|
@@ -28226,7 +28256,7 @@ var ChatStore = class {
|
|
|
28226
28256
|
let firstMessageAt = null;
|
|
28227
28257
|
let lastMessageAt = null;
|
|
28228
28258
|
let leftover = "";
|
|
28229
|
-
return new Promise((
|
|
28259
|
+
return new Promise((resolve4, reject) => {
|
|
28230
28260
|
stream.on("data", (chunk) => {
|
|
28231
28261
|
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
28232
28262
|
hash.update(text);
|
|
@@ -28266,7 +28296,7 @@ var ChatStore = class {
|
|
|
28266
28296
|
hash.update(metaRaw);
|
|
28267
28297
|
} catch {
|
|
28268
28298
|
}
|
|
28269
|
-
|
|
28299
|
+
resolve4({
|
|
28270
28300
|
transcriptHash: hash.digest("hex"),
|
|
28271
28301
|
hashAlgo: "sha256",
|
|
28272
28302
|
messageCount,
|
|
@@ -28301,7 +28331,398 @@ function isNotFound(err) {
|
|
|
28301
28331
|
return !!err && typeof err === "object" && err.code === "ENOENT";
|
|
28302
28332
|
}
|
|
28303
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
|
+
|
|
28304
28724
|
// src/chat-client.ts
|
|
28725
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
28305
28726
|
var DEFAULT_URL = "wss://chat.perkos.xyz/chat";
|
|
28306
28727
|
var DEFAULT_HISTORY_LIMIT = 50;
|
|
28307
28728
|
var DEFAULT_MIN_RECONNECT = 1e3;
|
|
@@ -28324,6 +28745,7 @@ var ChatClient = class {
|
|
|
28324
28745
|
connected = false;
|
|
28325
28746
|
authed = false;
|
|
28326
28747
|
stopped = false;
|
|
28748
|
+
generation = 0;
|
|
28327
28749
|
constructor(opts) {
|
|
28328
28750
|
this.agentName = opts.agentName;
|
|
28329
28751
|
const cfg = opts.config;
|
|
@@ -28359,20 +28781,43 @@ var ChatClient = class {
|
|
|
28359
28781
|
return;
|
|
28360
28782
|
}
|
|
28361
28783
|
this.stopped = false;
|
|
28362
|
-
this.
|
|
28784
|
+
const generation = ++this.generation;
|
|
28785
|
+
this.connect(generation);
|
|
28363
28786
|
}
|
|
28364
28787
|
stop() {
|
|
28365
|
-
|
|
28366
|
-
|
|
28367
|
-
|
|
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));
|
|
28368
28815
|
try {
|
|
28369
|
-
|
|
28816
|
+
socket.close(1e3, "client shutting down");
|
|
28370
28817
|
} catch {
|
|
28818
|
+
finish(false);
|
|
28371
28819
|
}
|
|
28372
|
-
|
|
28373
|
-
}
|
|
28374
|
-
this.connected = false;
|
|
28375
|
-
this.authed = false;
|
|
28820
|
+
});
|
|
28376
28821
|
}
|
|
28377
28822
|
/**
|
|
28378
28823
|
* Send a reply from this agent into a conversation. Appends to the local
|
|
@@ -28382,7 +28827,7 @@ var ChatClient = class {
|
|
|
28382
28827
|
* required (the simple agent loop should just regenerate from history).
|
|
28383
28828
|
*/
|
|
28384
28829
|
async sendReply(opts) {
|
|
28385
|
-
const id =
|
|
28830
|
+
const id = randomUUID6();
|
|
28386
28831
|
const msg = {
|
|
28387
28832
|
id,
|
|
28388
28833
|
from: `agent:${this.agentName}`,
|
|
@@ -28405,27 +28850,32 @@ var ChatClient = class {
|
|
|
28405
28850
|
// -------------------------------------------------------------------------
|
|
28406
28851
|
// WebSocket lifecycle
|
|
28407
28852
|
// -------------------------------------------------------------------------
|
|
28408
|
-
connect() {
|
|
28409
|
-
if (this.
|
|
28853
|
+
connect(generation) {
|
|
28854
|
+
if (!this.isCurrent(generation)) return;
|
|
28410
28855
|
const url = this.config.url;
|
|
28411
28856
|
this.logger.info(`[perkos-chat] connecting to ${url}`);
|
|
28412
28857
|
try {
|
|
28413
28858
|
this.ws = new wrapper_default(url);
|
|
28414
28859
|
} catch (err) {
|
|
28415
28860
|
this.logger.error(`[perkos-chat] failed to construct WS: ${errMsg(err)}`);
|
|
28416
|
-
this.scheduleReconnect();
|
|
28861
|
+
this.scheduleReconnect(generation);
|
|
28417
28862
|
return;
|
|
28418
28863
|
}
|
|
28864
|
+
const socket = this.ws;
|
|
28419
28865
|
let authTimer = setTimeout(() => {
|
|
28420
|
-
if (!this.authed) {
|
|
28866
|
+
if (this.isCurrent(generation, socket) && !this.authed) {
|
|
28421
28867
|
this.logger.error("[perkos-chat] auth timeout");
|
|
28422
28868
|
try {
|
|
28423
|
-
|
|
28869
|
+
socket.close(4002, "auth timeout");
|
|
28424
28870
|
} catch {
|
|
28425
28871
|
}
|
|
28426
28872
|
}
|
|
28427
28873
|
}, AUTH_TIMEOUT_MS);
|
|
28428
28874
|
this.ws.on("open", () => {
|
|
28875
|
+
if (!this.isCurrent(generation, socket)) {
|
|
28876
|
+
socket.close(1e3, "stale chat generation");
|
|
28877
|
+
return;
|
|
28878
|
+
}
|
|
28429
28879
|
this.connected = true;
|
|
28430
28880
|
this.reconnectMs = this.config.minReconnectMs;
|
|
28431
28881
|
this.sendFrame({
|
|
@@ -28436,6 +28886,7 @@ var ChatClient = class {
|
|
|
28436
28886
|
});
|
|
28437
28887
|
});
|
|
28438
28888
|
this.ws.on("message", (data) => {
|
|
28889
|
+
if (!this.isCurrent(generation, socket)) return;
|
|
28439
28890
|
let frame;
|
|
28440
28891
|
try {
|
|
28441
28892
|
frame = JSON.parse(data.toString("utf8"));
|
|
@@ -28450,19 +28901,21 @@ var ChatClient = class {
|
|
|
28450
28901
|
void this.handleFrame(frame);
|
|
28451
28902
|
});
|
|
28452
28903
|
this.ws.on("close", (code, reason) => {
|
|
28453
|
-
this.connected = false;
|
|
28454
|
-
this.authed = false;
|
|
28455
|
-
this.clearTimers();
|
|
28456
28904
|
if (authTimer) {
|
|
28457
28905
|
clearTimeout(authTimer);
|
|
28458
28906
|
authTimer = null;
|
|
28459
28907
|
}
|
|
28908
|
+
if (!this.isCurrent(generation, socket)) return;
|
|
28909
|
+
this.connected = false;
|
|
28910
|
+
this.authed = false;
|
|
28911
|
+
this.clearTimers();
|
|
28460
28912
|
if (!this.stopped) {
|
|
28461
28913
|
this.logger.info(`[perkos-chat] disconnected (${code}: ${reason?.toString() || "no reason"}); reconnecting`);
|
|
28462
|
-
this.scheduleReconnect();
|
|
28914
|
+
this.scheduleReconnect(generation);
|
|
28463
28915
|
}
|
|
28464
28916
|
});
|
|
28465
28917
|
this.ws.on("error", (err) => {
|
|
28918
|
+
if (!this.isCurrent(generation, socket)) return;
|
|
28466
28919
|
this.logger.error(`[perkos-chat] WS error: ${errMsg(err)}`);
|
|
28467
28920
|
});
|
|
28468
28921
|
}
|
|
@@ -28651,10 +29104,12 @@ var ChatClient = class {
|
|
|
28651
29104
|
}
|
|
28652
29105
|
}, this.config.heartbeatIntervalMs);
|
|
28653
29106
|
}
|
|
28654
|
-
scheduleReconnect() {
|
|
28655
|
-
if (this.
|
|
29107
|
+
scheduleReconnect(generation) {
|
|
29108
|
+
if (!this.isCurrent(generation)) return;
|
|
28656
29109
|
const delay = this.reconnectMs;
|
|
28657
|
-
this.reconnectTimer = setTimeout(() =>
|
|
29110
|
+
this.reconnectTimer = setTimeout(() => {
|
|
29111
|
+
if (this.isCurrent(generation)) this.connect(generation);
|
|
29112
|
+
}, delay);
|
|
28658
29113
|
this.reconnectMs = Math.min(delay * 2, this.config.maxReconnectMs);
|
|
28659
29114
|
}
|
|
28660
29115
|
clearTimers() {
|
|
@@ -28667,151 +29122,25 @@ var ChatClient = class {
|
|
|
28667
29122
|
this.reconnectTimer = null;
|
|
28668
29123
|
}
|
|
28669
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
|
+
}
|
|
28670
29139
|
};
|
|
28671
29140
|
function errMsg(err) {
|
|
28672
29141
|
return err instanceof Error ? err.message : String(err);
|
|
28673
29142
|
}
|
|
28674
29143
|
|
|
28675
|
-
// src/platform-heartbeat.ts
|
|
28676
|
-
var DEFAULT_INTERVAL_MS = 6e4;
|
|
28677
|
-
var MIN_INTERVAL_MS = 1e4;
|
|
28678
|
-
function startPlatformHeartbeat(opts) {
|
|
28679
|
-
const env = opts.env ?? process.env;
|
|
28680
|
-
const fetcher = opts.fetcher ?? fetch;
|
|
28681
|
-
const url = opts.url?.trim() || env.PERKOS_HEARTBEAT_URL?.trim();
|
|
28682
|
-
const relayKey = opts.relayKey?.trim() || env.A2A_RELAY_API_KEY?.trim();
|
|
28683
|
-
if (!url || !relayKey) return { stop: () => void 0 };
|
|
28684
|
-
const configuredInterval = Number(
|
|
28685
|
-
opts.intervalMs ?? env.PERKOS_HEARTBEAT_INTERVAL_MS ?? DEFAULT_INTERVAL_MS
|
|
28686
|
-
);
|
|
28687
|
-
const intervalMs = Number.isFinite(configuredInterval) ? Math.max(MIN_INTERVAL_MS, configuredInterval) : DEFAULT_INTERVAL_MS;
|
|
28688
|
-
const a2aRuntime = env.A2A_RUNTIME?.trim() || "hermes-api";
|
|
28689
|
-
const runtimeKind = opts.runtimeKind ?? (a2aRuntime === "openclaw" ? "openclaw" : a2aRuntime === "custom" ? "custom" : "hermes");
|
|
28690
|
-
let stopped = false;
|
|
28691
|
-
let firstSuccess = true;
|
|
28692
|
-
const report = async () => {
|
|
28693
|
-
if (stopped) return;
|
|
28694
|
-
try {
|
|
28695
|
-
const res = await fetcher(url, {
|
|
28696
|
-
method: "POST",
|
|
28697
|
-
headers: {
|
|
28698
|
-
"content-type": "application/json",
|
|
28699
|
-
authorization: `Bearer ${relayKey}`,
|
|
28700
|
-
"x-relay-key": relayKey
|
|
28701
|
-
},
|
|
28702
|
-
body: JSON.stringify({
|
|
28703
|
-
runtimeKind,
|
|
28704
|
-
version: opts.version?.trim() || env.PERKOS_A2A_VERSION?.trim() || "0.12.26",
|
|
28705
|
-
ts: Date.now()
|
|
28706
|
-
})
|
|
28707
|
-
});
|
|
28708
|
-
if (res.ok) {
|
|
28709
|
-
if (firstSuccess) {
|
|
28710
|
-
opts.logger.info(
|
|
28711
|
-
`[perkos-heartbeat] reported online to ${url} (status=${res.status}, every=${intervalMs}ms)`
|
|
28712
|
-
);
|
|
28713
|
-
firstSuccess = false;
|
|
28714
|
-
}
|
|
28715
|
-
} else {
|
|
28716
|
-
opts.logger.warn(
|
|
28717
|
-
`[perkos-heartbeat] ${res.status} from ${url} (continuing)`
|
|
28718
|
-
);
|
|
28719
|
-
}
|
|
28720
|
-
} catch (err) {
|
|
28721
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
28722
|
-
opts.logger.warn(
|
|
28723
|
-
`[perkos-heartbeat] POST failed to ${url}: ${msg} (continuing)`
|
|
28724
|
-
);
|
|
28725
|
-
}
|
|
28726
|
-
};
|
|
28727
|
-
void report();
|
|
28728
|
-
const timer = setInterval(() => void report(), intervalMs);
|
|
28729
|
-
timer.unref?.();
|
|
28730
|
-
return {
|
|
28731
|
-
stop: () => {
|
|
28732
|
-
stopped = true;
|
|
28733
|
-
clearInterval(timer);
|
|
28734
|
-
}
|
|
28735
|
-
};
|
|
28736
|
-
}
|
|
28737
|
-
|
|
28738
|
-
// src/runtime-evidence.ts
|
|
28739
|
-
import { createHmac } from "node:crypto";
|
|
28740
|
-
import {
|
|
28741
|
-
chmodSync,
|
|
28742
|
-
mkdirSync,
|
|
28743
|
-
readFileSync as readFileSync2,
|
|
28744
|
-
renameSync,
|
|
28745
|
-
writeFileSync
|
|
28746
|
-
} from "node:fs";
|
|
28747
|
-
import { homedir as homedir3 } from "node:os";
|
|
28748
|
-
import { join as join2, resolve } from "node:path";
|
|
28749
|
-
var RUNTIME_EVIDENCE_SCHEMA_VERSION = 1;
|
|
28750
|
-
function canonicalize(value) {
|
|
28751
|
-
if (Array.isArray(value)) return value.map(canonicalize);
|
|
28752
|
-
if (!value || typeof value !== "object") return value;
|
|
28753
|
-
return Object.fromEntries(
|
|
28754
|
-
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, child]) => [key, canonicalize(child)])
|
|
28755
|
-
);
|
|
28756
|
-
}
|
|
28757
|
-
function canonicalConfigJson(config) {
|
|
28758
|
-
return JSON.stringify({ schemaVersion: RUNTIME_EVIDENCE_SCHEMA_VERSION, config: canonicalize(config) });
|
|
28759
|
-
}
|
|
28760
|
-
function configFingerprint(config, hmacKeyHex) {
|
|
28761
|
-
if (!/^[a-f0-9]{64}$/u.test(hmacKeyHex)) throw new Error("runtime evidence HMAC key is invalid");
|
|
28762
|
-
return createHmac("sha256", Buffer.from(hmacKeyHex, "hex")).update(canonicalConfigJson(config)).digest("hex");
|
|
28763
|
-
}
|
|
28764
|
-
function runtimeEvidenceDir(stateDir) {
|
|
28765
|
-
return resolve(stateDir ?? process.env.OPENCLAW_STATE_DIR ?? join2(homedir3(), ".openclaw"), "perkos-a2a");
|
|
28766
|
-
}
|
|
28767
|
-
function runtimeAttemptPath(stateDir) {
|
|
28768
|
-
return join2(runtimeEvidenceDir(stateDir), "active-attempt.json");
|
|
28769
|
-
}
|
|
28770
|
-
function runtimeStatusPath(stateDir) {
|
|
28771
|
-
return join2(runtimeEvidenceDir(stateDir), "runtime-status.json");
|
|
28772
|
-
}
|
|
28773
|
-
function configAgentId(config) {
|
|
28774
|
-
if (config.platform?.agentId) return config.platform.agentId;
|
|
28775
|
-
const match = config.platform?.heartbeatUrl?.match(/\/agents\/([^/]+)\/heartbeat/u);
|
|
28776
|
-
return match?.[1] ?? "unknown";
|
|
28777
|
-
}
|
|
28778
|
-
function recordRuntimeLoadEvidence(input) {
|
|
28779
|
-
const evidenceDir = runtimeEvidenceDir(input.stateDir);
|
|
28780
|
-
let attempt;
|
|
28781
|
-
try {
|
|
28782
|
-
attempt = JSON.parse(readFileSync2(runtimeAttemptPath(input.stateDir), "utf8"));
|
|
28783
|
-
} catch {
|
|
28784
|
-
input.logger.info(`[perkos-a2a] config loaded version=${input.version} agentId=${configAgentId(input.config)} attemptId=none`);
|
|
28785
|
-
return null;
|
|
28786
|
-
}
|
|
28787
|
-
const agentId = configAgentId(input.config);
|
|
28788
|
-
if (attempt.schemaVersion !== RUNTIME_EVIDENCE_SCHEMA_VERSION || attempt.targetVersion !== input.version || attempt.expectedAgentId !== agentId) {
|
|
28789
|
-
input.logger.warn?.("[perkos-a2a] runtime evidence attempt does not match the loaded plugin identity");
|
|
28790
|
-
return null;
|
|
28791
|
-
}
|
|
28792
|
-
const status = {
|
|
28793
|
-
schemaVersion: RUNTIME_EVIDENCE_SCHEMA_VERSION,
|
|
28794
|
-
loadedAt: (input.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
28795
|
-
version: input.version,
|
|
28796
|
-
agentId,
|
|
28797
|
-
fingerprint: configFingerprint(input.config, attempt.hmacKeyHex),
|
|
28798
|
-
attemptId: attempt.attemptId,
|
|
28799
|
-
pid: process.pid,
|
|
28800
|
-
pluginRoot: resolve(input.pluginRoot)
|
|
28801
|
-
};
|
|
28802
|
-
mkdirSync(evidenceDir, { recursive: true, mode: 448 });
|
|
28803
|
-
const target = runtimeStatusPath(input.stateDir);
|
|
28804
|
-
const temporary = `${target}.${process.pid}.tmp`;
|
|
28805
|
-
writeFileSync(temporary, `${JSON.stringify(status, null, 2)}
|
|
28806
|
-
`, { mode: 384 });
|
|
28807
|
-
renameSync(temporary, target);
|
|
28808
|
-
chmodSync(target, 384);
|
|
28809
|
-
input.logger.info(
|
|
28810
|
-
`[perkos-a2a] config loaded version=${status.version} agentId=${status.agentId} attemptId=${status.attemptId} fingerprint=${status.fingerprint}`
|
|
28811
|
-
);
|
|
28812
|
-
return status;
|
|
28813
|
-
}
|
|
28814
|
-
|
|
28815
29144
|
// src/types.ts
|
|
28816
29145
|
function isWebhookEvent(msg) {
|
|
28817
29146
|
return msg.type === "webhook_event";
|
|
@@ -28820,7 +29149,7 @@ function isWebhookEvent(msg) {
|
|
|
28820
29149
|
// src/index.ts
|
|
28821
29150
|
function loadedPluginVersion() {
|
|
28822
29151
|
try {
|
|
28823
|
-
const manifest = JSON.parse(
|
|
29152
|
+
const manifest = JSON.parse(readFileSync4(new URL("../openclaw.plugin.json", import.meta.url), "utf8"));
|
|
28824
29153
|
return typeof manifest.version === "string" ? manifest.version : "unknown";
|
|
28825
29154
|
} catch {
|
|
28826
29155
|
return "unknown";
|
|
@@ -28915,7 +29244,7 @@ async function prepareOpenClawChatSession(api, config, sessionKey, logger) {
|
|
|
28915
29244
|
try {
|
|
28916
29245
|
await patchSessionEntry({
|
|
28917
29246
|
sessionKey,
|
|
28918
|
-
fallbackEntry: { sessionId:
|
|
29247
|
+
fallbackEntry: { sessionId: randomUUID7(), updatedAt: Date.now() },
|
|
28919
29248
|
preserveActivity: true,
|
|
28920
29249
|
replaceEntry: true,
|
|
28921
29250
|
update: (entry) => alignOpenClawChatSessionModel(entry, config.runtime?.model)
|
|
@@ -28935,7 +29264,7 @@ async function forceNativeOpenClawChatRuntime(api, config, sessionKey, logger) {
|
|
|
28935
29264
|
try {
|
|
28936
29265
|
await patchSessionEntry({
|
|
28937
29266
|
sessionKey,
|
|
28938
|
-
fallbackEntry: { sessionId:
|
|
29267
|
+
fallbackEntry: { sessionId: randomUUID7(), updatedAt: Date.now() },
|
|
28939
29268
|
preserveActivity: true,
|
|
28940
29269
|
replaceEntry: true,
|
|
28941
29270
|
update: (entry) => ({
|
|
@@ -28976,7 +29305,7 @@ async function runOpenClawChatTurn(api, config, sessionKey, prompt, transcriptPr
|
|
|
28976
29305
|
const runtimeConfig = currentConfig();
|
|
28977
29306
|
const workspaceDir = resolveAgentWorkspaceDir(runtimeConfig, agentId);
|
|
28978
29307
|
const entry = api.runtime?.agent?.session?.getSessionEntry?.({ agentId, sessionKey });
|
|
28979
|
-
const sessionId = typeof entry?.sessionId === "string" && entry.sessionId.trim() ? entry.sessionId :
|
|
29308
|
+
const sessionId = typeof entry?.sessionId === "string" && entry.sessionId.trim() ? entry.sessionId : randomUUID7();
|
|
28980
29309
|
const configuredTimeout = api.runtime?.agent?.resolveAgentTimeoutMs?.(runtimeConfig);
|
|
28981
29310
|
const timeoutMs = Math.min(
|
|
28982
29311
|
75e3,
|
|
@@ -28991,7 +29320,7 @@ async function runOpenClawChatTurn(api, config, sessionKey, prompt, transcriptPr
|
|
|
28991
29320
|
prompt,
|
|
28992
29321
|
transcriptPrompt,
|
|
28993
29322
|
timeoutMs,
|
|
28994
|
-
runId:
|
|
29323
|
+
runId: randomUUID7(),
|
|
28995
29324
|
trigger: "user",
|
|
28996
29325
|
messageChannel: "perkos-chat",
|
|
28997
29326
|
disableTools: true,
|
|
@@ -29077,11 +29406,6 @@ async function deliverToHermes(config, message, logger) {
|
|
|
29077
29406
|
}
|
|
29078
29407
|
logger.info(`[perkos-a2a] Delivered inbound A2A task to Hermes API session ${sessionKey} via ${url}`);
|
|
29079
29408
|
}
|
|
29080
|
-
var A2A_LIFECYCLE_REGISTRY = /* @__PURE__ */ Symbol.for("@perkos/perkos-a2a:lifecycle-registry");
|
|
29081
|
-
function lifecycleRegistry() {
|
|
29082
|
-
const root = globalThis;
|
|
29083
|
-
return root[A2A_LIFECYCLE_REGISTRY] ??= /* @__PURE__ */ new Map();
|
|
29084
|
-
}
|
|
29085
29409
|
function a2aLifecycleKey(config) {
|
|
29086
29410
|
const rawRelayUrl = (config.relay?.url || "no-relay").trim();
|
|
29087
29411
|
let relayUrl = rawRelayUrl.replace(/[?#].*$/u, "").replace(/\/+$/u, "").toLowerCase();
|
|
@@ -29111,12 +29435,12 @@ function register(api) {
|
|
|
29111
29435
|
recordRuntimeLoadEvidence({
|
|
29112
29436
|
config: pluginConfig,
|
|
29113
29437
|
version: pluginVersion,
|
|
29114
|
-
pluginRoot:
|
|
29438
|
+
pluginRoot: resolve3(dirname2(fileURLToPath2(import.meta.url)), ".."),
|
|
29115
29439
|
logger
|
|
29116
29440
|
});
|
|
29117
29441
|
const server = new A2AServer(pluginConfig, logger);
|
|
29118
29442
|
const lifecycleKey = a2aLifecycleKey(pluginConfig);
|
|
29119
|
-
const lifecycleInstanceId =
|
|
29443
|
+
const lifecycleInstanceId = randomUUID7();
|
|
29120
29444
|
const pendingTasks = [];
|
|
29121
29445
|
const enqueueSystemEvent = api.runtime?.system?.enqueueSystemEvent;
|
|
29122
29446
|
const requestHeartbeatNow = api.runtime?.system?.requestHeartbeatNow;
|
|
@@ -29171,7 +29495,7 @@ function register(api) {
|
|
|
29171
29495
|
await deliverToHermes(pluginConfig, eventText, logger);
|
|
29172
29496
|
task.artifacts.push({
|
|
29173
29497
|
kind: "artifact",
|
|
29174
|
-
artifactId:
|
|
29498
|
+
artifactId: randomUUID7(),
|
|
29175
29499
|
parts: [{ kind: "text", text: `debug: delivered task ${task.id} to Hermes API Server` }]
|
|
29176
29500
|
});
|
|
29177
29501
|
const finalText2 = await generateRuntimeReply(pluginConfig, text, marker);
|
|
@@ -29181,14 +29505,14 @@ function register(api) {
|
|
|
29181
29505
|
if (runtimeKind === "none") {
|
|
29182
29506
|
task.artifacts.push({
|
|
29183
29507
|
kind: "artifact",
|
|
29184
|
-
artifactId:
|
|
29508
|
+
artifactId: randomUUID7(),
|
|
29185
29509
|
parts: [{ kind: "text", text: `debug: runtime delivery disabled for ${marker}` }]
|
|
29186
29510
|
});
|
|
29187
29511
|
return;
|
|
29188
29512
|
}
|
|
29189
29513
|
task.artifacts.push({
|
|
29190
29514
|
kind: "artifact",
|
|
29191
|
-
artifactId:
|
|
29515
|
+
artifactId: randomUUID7(),
|
|
29192
29516
|
parts: [{ kind: "text", text: `debug: enqueued system event for ${marker}` }]
|
|
29193
29517
|
});
|
|
29194
29518
|
if (enqueueSystemEvent) {
|
|
@@ -29272,7 +29596,7 @@ function register(api) {
|
|
|
29272
29596
|
if (!finalText) continue;
|
|
29273
29597
|
task.artifacts.push({
|
|
29274
29598
|
kind: "artifact",
|
|
29275
|
-
artifactId:
|
|
29599
|
+
artifactId: randomUUID7(),
|
|
29276
29600
|
parts: [{ kind: "text", text: finalText }]
|
|
29277
29601
|
});
|
|
29278
29602
|
task.status = {
|
|
@@ -29286,179 +29610,137 @@ function register(api) {
|
|
|
29286
29610
|
logger.info(`[perkos-a2a] Task ${task.id} completed from transcript update`);
|
|
29287
29611
|
}
|
|
29288
29612
|
});
|
|
29613
|
+
const chatApiKey = pluginConfig.chat?.apiKey || pluginConfig.relay?.apiKey;
|
|
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
|
|
29684
|
+
});
|
|
29685
|
+
logger.info(`[perkos-chat] embedded reply ${result.delivered ? "delivered" : "queued (offline)"} for ${frame.convId}`);
|
|
29686
|
+
return;
|
|
29687
|
+
}
|
|
29688
|
+
}
|
|
29689
|
+
if (enqueueSystemEvent) enqueueSystemEvent(eventText, { sessionKey: chatSessionKey });
|
|
29690
|
+
wakeGatewayAgent(requestHeartbeatNow, `[PerkOS-Chat] inbound message in ${frame.convId}`, logger, chatSessionKey);
|
|
29691
|
+
};
|
|
29692
|
+
if (pluginConfig.chat?.enabled && !chatApiKey) {
|
|
29693
|
+
logger.error("[perkos-chat] chat.enabled=true but no apiKey (set chat.apiKey or fall back from relay.apiKey)");
|
|
29694
|
+
}
|
|
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)`);
|
|
29727
|
+
}
|
|
29728
|
+
}
|
|
29729
|
+
});
|
|
29730
|
+
server.setRelayTransport(supervisorClient);
|
|
29289
29731
|
api.registerService({
|
|
29290
29732
|
id: "perkos-a2a",
|
|
29291
29733
|
start: async () => {
|
|
29292
|
-
|
|
29293
|
-
const previous = registry.get(lifecycleKey);
|
|
29294
|
-
if (previous && previous.instanceId !== lifecycleInstanceId) {
|
|
29295
|
-
logger.info(`[perkos-a2a] Waiting for previous relay generation to stop for ${pluginConfig.agentName}`);
|
|
29296
|
-
await previous.server.stop({ reason: "replaced", timeoutMs: 5e3, drainMs: 3e3 });
|
|
29297
|
-
}
|
|
29298
|
-
registry.set(lifecycleKey, { instanceId: lifecycleInstanceId, server });
|
|
29734
|
+
await supervisorClient.start();
|
|
29299
29735
|
await server.start();
|
|
29300
29736
|
logger.info(`[perkos-a2a] A2A server started for ${pluginConfig.agentName}`);
|
|
29301
29737
|
},
|
|
29302
29738
|
stop: async () => {
|
|
29303
29739
|
logger.info("[perkos-a2a] A2A server stopping");
|
|
29304
|
-
await server.stop({ reason: "openclaw-service-stop", timeoutMs:
|
|
29305
|
-
|
|
29306
|
-
if (registry.get(lifecycleKey)?.instanceId === lifecycleInstanceId) registry.delete(lifecycleKey);
|
|
29740
|
+
await server.stop({ reason: "openclaw-service-stop", timeoutMs: 8e3, drainMs: 3e3 });
|
|
29741
|
+
await supervisorClient.stop();
|
|
29307
29742
|
}
|
|
29308
29743
|
});
|
|
29309
|
-
const chatApiKey = pluginConfig.chat?.apiKey || pluginConfig.relay?.apiKey;
|
|
29310
|
-
const chatClient = pluginConfig.chat?.enabled && chatApiKey ? new ChatClient({
|
|
29311
|
-
agentName: pluginConfig.agentName,
|
|
29312
|
-
config: {
|
|
29313
|
-
enabled: true,
|
|
29314
|
-
url: pluginConfig.chat?.url || "wss://chat.perkos.xyz/chat",
|
|
29315
|
-
apiKey: chatApiKey,
|
|
29316
|
-
storeRoot: pluginConfig.chat?.storeRoot,
|
|
29317
|
-
defaultHistoryLimit: pluginConfig.chat?.defaultHistoryLimit,
|
|
29318
|
-
minReconnectMs: pluginConfig.chat?.minReconnectMs,
|
|
29319
|
-
maxReconnectMs: pluginConfig.chat?.maxReconnectMs,
|
|
29320
|
-
heartbeatIntervalMs: pluginConfig.chat?.heartbeatIntervalMs
|
|
29321
|
-
},
|
|
29322
|
-
handlers: {
|
|
29323
|
-
logger,
|
|
29324
|
-
onChatDeliver: async (frame) => {
|
|
29325
|
-
const walletAddress = parseWalletFromIdentity(frame.from);
|
|
29326
|
-
const marker = `[PERKOS_CHAT:${frame.convId}]`;
|
|
29327
|
-
const recentPage = await chatClient?.getStore().readPage(frame.convId, {
|
|
29328
|
-
limit: CHAT_CONTEXT_MESSAGE_LIMIT + 1
|
|
29329
|
-
});
|
|
29330
|
-
const recentContext = formatRecentChatContext(
|
|
29331
|
-
recentPage?.messages ?? [],
|
|
29332
|
-
frame.id
|
|
29333
|
-
);
|
|
29334
|
-
const eventText = [
|
|
29335
|
-
marker,
|
|
29336
|
-
`From: ${frame.from}`,
|
|
29337
|
-
`Conversation: ${frame.convId}`,
|
|
29338
|
-
frame.projectId ? `Canonical projectId: ${frame.projectId}` : "",
|
|
29339
|
-
walletAddress ? `Wallet: ${walletAddress}` : "",
|
|
29340
|
-
"",
|
|
29341
|
-
"A user is messaging you in a PerkOS chat conversation.",
|
|
29342
|
-
"To respond, call the `perkos_chat_reply` tool with:",
|
|
29343
|
-
` convId: "${frame.convId}"`,
|
|
29344
|
-
walletAddress ? ` walletAddress: "${walletAddress}"` : ` walletAddress: <derived from From>`,
|
|
29345
|
-
` text: <your reply>`,
|
|
29346
|
-
"",
|
|
29347
|
-
"Recent conversation context (oldest to newest):",
|
|
29348
|
-
recentContext,
|
|
29349
|
-
"",
|
|
29350
|
-
"Use the recent context. Do not ask again for information the user already provided.",
|
|
29351
|
-
"",
|
|
29352
|
-
"Message body:",
|
|
29353
|
-
frame.text
|
|
29354
|
-
].filter(Boolean).join("\n");
|
|
29355
|
-
const directRunPrompt = [
|
|
29356
|
-
marker,
|
|
29357
|
-
`From: ${frame.from}`,
|
|
29358
|
-
`Conversation: ${frame.convId}`,
|
|
29359
|
-
frame.projectId ? `Canonical projectId: ${frame.projectId}` : "",
|
|
29360
|
-
"",
|
|
29361
|
-
"Reply to this PerkOS chat message using your current runtime configuration.",
|
|
29362
|
-
"Return only the reply text. Do not call messaging tools; the PerkOS plugin delivers the returned text.",
|
|
29363
|
-
"Use the recent context and do not ask again for information already provided.",
|
|
29364
|
-
"",
|
|
29365
|
-
"Recent conversation context (oldest to newest):",
|
|
29366
|
-
recentContext,
|
|
29367
|
-
"",
|
|
29368
|
-
"Message body:",
|
|
29369
|
-
frame.text
|
|
29370
|
-
].filter(Boolean).join("\n");
|
|
29371
|
-
const runtimeKind = pluginConfig.runtime?.kind || "openclaw";
|
|
29372
|
-
if (runtimeKind === "hermes" || runtimeKind === "hermes-api") {
|
|
29373
|
-
try {
|
|
29374
|
-
await deliverToHermes(pluginConfig, eventText, logger);
|
|
29375
|
-
} catch (err) {
|
|
29376
|
-
logger.error(`[perkos-chat] hermes delivery failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
29377
|
-
}
|
|
29378
|
-
return;
|
|
29379
|
-
}
|
|
29380
|
-
if (runtimeKind === "none") {
|
|
29381
|
-
logger.info(`[perkos-chat] dropped chat_deliver for ${frame.convId} (runtime=none)`);
|
|
29382
|
-
return;
|
|
29383
|
-
}
|
|
29384
|
-
const chatSessionKey = openclawChatSessionKey(pluginConfig, frame.convId);
|
|
29385
|
-
await prepareOpenClawChatSession(api, pluginConfig, chatSessionKey, logger);
|
|
29386
|
-
if (walletAddress) {
|
|
29387
|
-
const reply = await runOpenClawChatTurn(
|
|
29388
|
-
api,
|
|
29389
|
-
pluginConfig,
|
|
29390
|
-
chatSessionKey,
|
|
29391
|
-
directRunPrompt,
|
|
29392
|
-
frame.text,
|
|
29393
|
-
logger
|
|
29394
|
-
);
|
|
29395
|
-
if (reply) {
|
|
29396
|
-
const result = await chatClient?.sendReply({
|
|
29397
|
-
convId: frame.convId,
|
|
29398
|
-
walletAddress,
|
|
29399
|
-
text: reply,
|
|
29400
|
-
replyTo: frame.id
|
|
29401
|
-
});
|
|
29402
|
-
logger.info(
|
|
29403
|
-
`[perkos-chat] embedded reply ${result?.delivered ? "delivered" : "queued (offline)"} for ${frame.convId}`
|
|
29404
|
-
);
|
|
29405
|
-
return;
|
|
29406
|
-
}
|
|
29407
|
-
}
|
|
29408
|
-
if (enqueueSystemEvent) {
|
|
29409
|
-
enqueueSystemEvent(eventText, { sessionKey: chatSessionKey });
|
|
29410
|
-
}
|
|
29411
|
-
wakeGatewayAgent(
|
|
29412
|
-
requestHeartbeatNow,
|
|
29413
|
-
`[PerkOS-Chat] inbound message in ${frame.convId}`,
|
|
29414
|
-
logger,
|
|
29415
|
-
chatSessionKey
|
|
29416
|
-
);
|
|
29417
|
-
},
|
|
29418
|
-
onChannelJoin: async (frame) => {
|
|
29419
|
-
logger.info(`[perkos-chat] joined conv ${frame.convId} (host: ${frame.historyHost}, ${frame.participants.length} participants)`);
|
|
29420
|
-
}
|
|
29421
|
-
}
|
|
29422
|
-
}) : null;
|
|
29423
|
-
if (pluginConfig.chat?.enabled && !chatApiKey) {
|
|
29424
|
-
logger.error("[perkos-chat] chat.enabled=true but no apiKey (set chat.apiKey or fall back from relay.apiKey)");
|
|
29425
|
-
}
|
|
29426
|
-
if (chatClient) {
|
|
29427
|
-
api.registerService({
|
|
29428
|
-
id: "perkos-a2a-chat",
|
|
29429
|
-
start: () => {
|
|
29430
|
-
chatClient.start();
|
|
29431
|
-
logger.info(`[perkos-chat] client started for ${pluginConfig.agentName}`);
|
|
29432
|
-
},
|
|
29433
|
-
stop: () => {
|
|
29434
|
-
chatClient.stop();
|
|
29435
|
-
logger.info("[perkos-chat] client stopped");
|
|
29436
|
-
}
|
|
29437
|
-
});
|
|
29438
|
-
}
|
|
29439
|
-
if (pluginConfig.platform?.heartbeatUrl) {
|
|
29440
|
-
let platformHeartbeat = null;
|
|
29441
|
-
api.registerService({
|
|
29442
|
-
id: "perkos-a2a-platform-heartbeat",
|
|
29443
|
-
start: () => {
|
|
29444
|
-
platformHeartbeat = startPlatformHeartbeat({
|
|
29445
|
-
logger: {
|
|
29446
|
-
info: (message) => logger.info(message),
|
|
29447
|
-
warn: (message) => (logger.warn || logger.info).call(logger, message)
|
|
29448
|
-
},
|
|
29449
|
-
url: pluginConfig.platform?.heartbeatUrl,
|
|
29450
|
-
relayKey: pluginConfig.relay?.apiKey,
|
|
29451
|
-
runtimeKind: "openclaw",
|
|
29452
|
-
version: pluginVersion,
|
|
29453
|
-
intervalMs: pluginConfig.platform?.heartbeatIntervalMs
|
|
29454
|
-
});
|
|
29455
|
-
},
|
|
29456
|
-
stop: () => {
|
|
29457
|
-
platformHeartbeat?.stop();
|
|
29458
|
-
platformHeartbeat = null;
|
|
29459
|
-
}
|
|
29460
|
-
});
|
|
29461
|
-
}
|
|
29462
29744
|
api.registerTool({
|
|
29463
29745
|
name: "perkos_a2a_send",
|
|
29464
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.",
|
|
@@ -29645,13 +29927,13 @@ function register(api) {
|
|
|
29645
29927
|
required: ["convId", "walletAddress", "text"]
|
|
29646
29928
|
},
|
|
29647
29929
|
async execute(_id, params) {
|
|
29648
|
-
if (!
|
|
29930
|
+
if (!chatEnabled) {
|
|
29649
29931
|
return {
|
|
29650
29932
|
content: [{ type: "text", text: "PerkOS-Chat client is not enabled in this agent's config." }]
|
|
29651
29933
|
};
|
|
29652
29934
|
}
|
|
29653
29935
|
try {
|
|
29654
|
-
const result = await
|
|
29936
|
+
const result = await supervisorClient.sendChatReply({
|
|
29655
29937
|
convId: params.convId,
|
|
29656
29938
|
walletAddress: params.walletAddress,
|
|
29657
29939
|
text: params.text,
|
|
@@ -29692,14 +29974,13 @@ function register(api) {
|
|
|
29692
29974
|
required: ["convId"]
|
|
29693
29975
|
},
|
|
29694
29976
|
async execute(_id, params) {
|
|
29695
|
-
if (!
|
|
29977
|
+
if (!chatStore) {
|
|
29696
29978
|
return {
|
|
29697
29979
|
content: [{ type: "text", text: "PerkOS-Chat client is not enabled in this agent's config." }]
|
|
29698
29980
|
};
|
|
29699
29981
|
}
|
|
29700
29982
|
try {
|
|
29701
|
-
const
|
|
29702
|
-
const page = await store.readPage(params.convId, {
|
|
29983
|
+
const page = await chatStore.readPage(params.convId, {
|
|
29703
29984
|
before: params.before ?? null,
|
|
29704
29985
|
limit: params.limit ?? 50
|
|
29705
29986
|
});
|
|
@@ -29728,9 +30009,9 @@ function register(api) {
|
|
|
29728
30009
|
hasSystemEventInjection: !!enqueueSystemEvent,
|
|
29729
30010
|
protocol: "a2a",
|
|
29730
30011
|
version: "0.8.4",
|
|
29731
|
-
chat:
|
|
30012
|
+
chat: chatEnabled ? {
|
|
29732
30013
|
enabled: true,
|
|
29733
|
-
connected:
|
|
30014
|
+
connected: supervisorClient.isChatConnected(),
|
|
29734
30015
|
url: pluginConfig.chat?.url || "wss://chat.perkos.xyz/chat"
|
|
29735
30016
|
} : { enabled: false, connected: false, url: null }
|
|
29736
30017
|
});
|
|
@@ -29748,8 +30029,8 @@ function register(api) {
|
|
|
29748
30029
|
console.log(`Auth required: ${pluginConfig.auth?.requireApiKey || false}`);
|
|
29749
30030
|
console.log(`Peers: ${Object.keys(pluginConfig.peers).join(", ") || "(none)"}`);
|
|
29750
30031
|
console.log(`System event injection: ${!!enqueueSystemEvent}`);
|
|
29751
|
-
if (
|
|
29752
|
-
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"})`);
|
|
29753
30034
|
} else {
|
|
29754
30035
|
console.log("Chat: disabled");
|
|
29755
30036
|
}
|
|
@@ -29770,26 +30051,26 @@ function register(api) {
|
|
|
29770
30051
|
console.log("[perkos-a2a] Detecting environment...\n");
|
|
29771
30052
|
const isMac = process.platform === "darwin";
|
|
29772
30053
|
console.log(`Platform: ${process.platform}${isMac ? " (macOS)" : ""}`);
|
|
29773
|
-
const
|
|
29774
|
-
console.log(`Public IP: ${
|
|
29775
|
-
console.log(`Local IPs: ${
|
|
29776
|
-
console.log(`Behind NAT: ${
|
|
29777
|
-
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})` : ""}`);
|
|
29778
30059
|
let portAvailable = true;
|
|
29779
30060
|
try {
|
|
29780
30061
|
const netMod = await import("net");
|
|
29781
|
-
await new Promise((
|
|
30062
|
+
await new Promise((resolve4, reject) => {
|
|
29782
30063
|
const srv = netMod.createServer();
|
|
29783
30064
|
srv.once("error", (err) => {
|
|
29784
30065
|
if (err.code === "EADDRINUSE") {
|
|
29785
30066
|
portAvailable = false;
|
|
29786
|
-
|
|
30067
|
+
resolve4();
|
|
29787
30068
|
} else {
|
|
29788
30069
|
reject(err);
|
|
29789
30070
|
}
|
|
29790
30071
|
});
|
|
29791
30072
|
srv.listen(pluginConfig.port, () => {
|
|
29792
|
-
srv.close(() =>
|
|
30073
|
+
srv.close(() => resolve4());
|
|
29793
30074
|
});
|
|
29794
30075
|
});
|
|
29795
30076
|
} catch {
|
|
@@ -29809,14 +30090,14 @@ Relay: ${pluginConfig.relay?.enabled ? "enabled" : "disabled"}`);
|
|
|
29809
30090
|
console.log(` Local loopback URL for this agent: http://127.0.0.1:${pluginConfig.port}/a2a/jsonrpc`);
|
|
29810
30091
|
console.log(" Configure same-host peers as http://127.0.0.1:<peer-port>, not by shared public IP.");
|
|
29811
30092
|
console.log("");
|
|
29812
|
-
if (!
|
|
30093
|
+
if (!net2.isBehindNat) {
|
|
29813
30094
|
console.log("You have a public IP. Use it only for remote peers that cannot reach a local/tunnel URL.");
|
|
29814
|
-
console.log(` Public A2A URL candidate: http://${
|
|
30095
|
+
console.log(` Public A2A URL candidate: http://${net2.publicIp}:${pluginConfig.port}/a2a/jsonrpc`);
|
|
29815
30096
|
} else if (pluginConfig.relay?.enabled) {
|
|
29816
30097
|
console.log("You are behind NAT but relay is configured. Bidirectional communication via relay.");
|
|
29817
|
-
} else if (
|
|
30098
|
+
} else if (net2.hasTailscale && net2.tailscaleIp) {
|
|
29818
30099
|
console.log("Use your Tailscale IP for peers.");
|
|
29819
|
-
console.log(` Your A2A URL: http://${
|
|
30100
|
+
console.log(` Your A2A URL: http://${net2.tailscaleIp}:${pluginConfig.port}/a2a/jsonrpc`);
|
|
29820
30101
|
} else {
|
|
29821
30102
|
console.log("You are behind NAT. Options:");
|
|
29822
30103
|
console.log(" 1) Configure a relay hub (recommended) - set relay.url and relay.enabled in config");
|