@perkos/perkos-a2a 0.12.31 → 0.12.33
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 +41 -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 +759 -424
- 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 +43 -0
- package/dist/openclaw-supervisor-cli.js.map +1 -0
- package/dist/openclaw-supervisor-client.d.ts +71 -0
- package/dist/openclaw-supervisor-client.d.ts.map +1 -0
- package/dist/openclaw-supervisor-client.js +343 -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 +447 -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 +14 -2
- package/dist/repair.d.ts.map +1 -1
- package/dist/repair.js +126 -44
- package/dist/repair.js.map +1 -1
- package/dist/runtime-evidence.d.ts +6 -3
- package/dist/runtime-evidence.d.ts.map +1 -1
- package/dist/runtime-evidence.js +7 -1
- package/dist/runtime-evidence.js.map +1 -1
- package/dist/server.d.ts +20 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +47 -7
- package/dist/server.js.map +1 -1
- package/dist/supervisor-protocol.d.ts +138 -0
- package/dist/supervisor-protocol.d.ts.map +1 -0
- package/dist/supervisor-protocol.js +60 -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,8 @@ var A2AServer = class {
|
|
|
27008
27008
|
logger;
|
|
27009
27009
|
clientOnly = false;
|
|
27010
27010
|
relayClient = null;
|
|
27011
|
+
externalRelayTransport = false;
|
|
27012
|
+
relayOwnership;
|
|
27011
27013
|
relayHub = null;
|
|
27012
27014
|
messageInjector = null;
|
|
27013
27015
|
taskResultHandler = null;
|
|
@@ -27019,9 +27021,10 @@ var A2AServer = class {
|
|
|
27019
27021
|
startPromise = null;
|
|
27020
27022
|
stopPromise = null;
|
|
27021
27023
|
operations = /* @__PURE__ */ new Set();
|
|
27022
|
-
constructor(config, logger) {
|
|
27024
|
+
constructor(config, logger, options = {}) {
|
|
27023
27025
|
this.config = config;
|
|
27024
27026
|
this.logger = logger || { info: console.log, error: console.error };
|
|
27027
|
+
this.relayOwnership = options.relayOwnership ?? "local";
|
|
27025
27028
|
this.app = (0, import_express.default)();
|
|
27026
27029
|
this.app.use(import_express.default.json());
|
|
27027
27030
|
this.agentCard = {
|
|
@@ -27047,6 +27050,14 @@ var A2AServer = class {
|
|
|
27047
27050
|
setTaskFailureHandler(handler) {
|
|
27048
27051
|
this.taskFailureHandler = handler;
|
|
27049
27052
|
}
|
|
27053
|
+
/** Use a process-external owner for relay transport (OpenClaw supervisor). */
|
|
27054
|
+
setRelayTransport(transport) {
|
|
27055
|
+
if (this.lifecycleState !== "new" && this.lifecycleState !== "stopped") {
|
|
27056
|
+
throw new Error("relay transport must be configured before start");
|
|
27057
|
+
}
|
|
27058
|
+
this.relayClient = transport;
|
|
27059
|
+
this.externalRelayTransport = true;
|
|
27060
|
+
}
|
|
27050
27061
|
getTasks() {
|
|
27051
27062
|
return this.tasks.values();
|
|
27052
27063
|
}
|
|
@@ -27062,6 +27073,9 @@ var A2AServer = class {
|
|
|
27062
27073
|
isRelayConnected() {
|
|
27063
27074
|
return this.relayClient?.isConnected() || false;
|
|
27064
27075
|
}
|
|
27076
|
+
getAgentCard() {
|
|
27077
|
+
return structuredClone(this.agentCard);
|
|
27078
|
+
}
|
|
27065
27079
|
authMiddleware() {
|
|
27066
27080
|
return (req, res, next) => {
|
|
27067
27081
|
if (!this.config.auth?.requireApiKey) {
|
|
@@ -27324,7 +27338,7 @@ var A2AServer = class {
|
|
|
27324
27338
|
async waitForRemoteTaskTerminal(targetUrl, headers, taskId, timeoutMs = 45e3, pollMs = 1e3) {
|
|
27325
27339
|
const deadline = Date.now() + timeoutMs;
|
|
27326
27340
|
while (Date.now() < deadline) {
|
|
27327
|
-
await new Promise((
|
|
27341
|
+
await new Promise((resolve4) => setTimeout(resolve4, pollMs));
|
|
27328
27342
|
const response = await fetch(`${targetUrl}/a2a/jsonrpc`, {
|
|
27329
27343
|
method: "POST",
|
|
27330
27344
|
headers,
|
|
@@ -27348,7 +27362,7 @@ var A2AServer = class {
|
|
|
27348
27362
|
if (task && isTerminalTaskState(task.status?.state)) {
|
|
27349
27363
|
return this.success(randomUUID4(), structuredClone(task));
|
|
27350
27364
|
}
|
|
27351
|
-
await new Promise((
|
|
27365
|
+
await new Promise((resolve4) => setTimeout(resolve4, pollMs));
|
|
27352
27366
|
}
|
|
27353
27367
|
return null;
|
|
27354
27368
|
}
|
|
@@ -27454,9 +27468,33 @@ var A2AServer = class {
|
|
|
27454
27468
|
});
|
|
27455
27469
|
this.trackOperation(operation);
|
|
27456
27470
|
}
|
|
27457
|
-
|
|
27471
|
+
/** Process a relay task delivered by the OpenClaw supervisor over IPC. */
|
|
27472
|
+
async handleTransportTask(msg) {
|
|
27473
|
+
if (this.lifecycleState !== "running") {
|
|
27474
|
+
return this.error(String(msg.payload?.id ?? msg.id), 503, "SERVER_RELOADING");
|
|
27475
|
+
}
|
|
27476
|
+
const payload = msg.payload;
|
|
27477
|
+
const params = payload.params || payload;
|
|
27478
|
+
const rpcId = String(payload.id || msg.id);
|
|
27479
|
+
const response = await this.handleSendMessage(params, rpcId);
|
|
27480
|
+
const task = response.result;
|
|
27481
|
+
if (task?.id && !isTerminalTaskState(task.status?.state)) {
|
|
27482
|
+
const terminal = await this.waitForLocalTaskTerminal(task.id);
|
|
27483
|
+
if (terminal) return terminal;
|
|
27484
|
+
}
|
|
27485
|
+
return response;
|
|
27486
|
+
}
|
|
27487
|
+
async startRelayClient() {
|
|
27458
27488
|
const relay = this.config.relay;
|
|
27459
27489
|
if (!relay?.enabled || !relay?.url) return;
|
|
27490
|
+
if (this.externalRelayTransport && this.relayClient) {
|
|
27491
|
+
await this.relayClient.start();
|
|
27492
|
+
this.logger.info(`[perkos-a2a] Relay transport delegated to supervisor for ${relay.url}`);
|
|
27493
|
+
return;
|
|
27494
|
+
}
|
|
27495
|
+
if (this.relayOwnership === "external-required") {
|
|
27496
|
+
throw new Error("OWNERSHIP_PRIMITIVE_UNAVAILABLE:OpenClaw relay transport requires the process-external supervisor");
|
|
27497
|
+
}
|
|
27460
27498
|
this.relayClient = new RelayClient({
|
|
27461
27499
|
agentName: this.config.agentName,
|
|
27462
27500
|
relay,
|
|
@@ -27509,7 +27547,7 @@ var A2AServer = class {
|
|
|
27509
27547
|
if (mode === "client-only") {
|
|
27510
27548
|
if (!this.isGenerationActive(generation, "starting")) return;
|
|
27511
27549
|
this.clientOnly = true;
|
|
27512
|
-
this.startRelayClient();
|
|
27550
|
+
await this.startRelayClient();
|
|
27513
27551
|
if (!this.config.relay?.enabled) {
|
|
27514
27552
|
this.logger.info(
|
|
27515
27553
|
"[perkos-a2a] Running in client-only mode. Configure relay for NAT traversal or set up Tailscale/tunnel."
|
|
@@ -27520,19 +27558,19 @@ var A2AServer = class {
|
|
|
27520
27558
|
if (mode === "full") {
|
|
27521
27559
|
await this.tryListen(this.config.port, 1, generation);
|
|
27522
27560
|
if (!this.isGenerationActive(generation, "starting")) return;
|
|
27523
|
-
this.startRelayClient();
|
|
27561
|
+
await this.startRelayClient();
|
|
27524
27562
|
return;
|
|
27525
27563
|
}
|
|
27526
27564
|
try {
|
|
27527
|
-
const
|
|
27565
|
+
const net2 = await detectNetworking();
|
|
27528
27566
|
if (!this.isGenerationActive(generation, "starting")) return;
|
|
27529
|
-
if (
|
|
27567
|
+
if (net2.isBehindNat && !net2.hasTailscale) {
|
|
27530
27568
|
if (this.config.relay?.enabled) {
|
|
27531
27569
|
this.logger.info(
|
|
27532
27570
|
"[perkos-a2a] Behind NAT, using relay for bidirectional communication"
|
|
27533
27571
|
);
|
|
27534
27572
|
this.clientOnly = true;
|
|
27535
|
-
this.startRelayClient();
|
|
27573
|
+
await this.startRelayClient();
|
|
27536
27574
|
} else {
|
|
27537
27575
|
this.logger.info(
|
|
27538
27576
|
"[perkos-a2a] Running in client-only mode (behind NAT). Configure relay or Tailscale for bidirectional A2A."
|
|
@@ -27541,16 +27579,16 @@ var A2AServer = class {
|
|
|
27541
27579
|
}
|
|
27542
27580
|
return;
|
|
27543
27581
|
}
|
|
27544
|
-
if (
|
|
27582
|
+
if (net2.isBehindNat && net2.hasTailscale && net2.tailscaleIp) {
|
|
27545
27583
|
this.logger.info(
|
|
27546
|
-
`[perkos-a2a] Behind NAT but Tailscale detected (${
|
|
27584
|
+
`[perkos-a2a] Behind NAT but Tailscale detected (${net2.tailscaleIp}). Starting server.`
|
|
27547
27585
|
);
|
|
27548
27586
|
}
|
|
27549
27587
|
} catch {
|
|
27550
27588
|
}
|
|
27551
27589
|
await this.tryListen(this.config.port, 1, generation);
|
|
27552
27590
|
if (!this.isGenerationActive(generation, "starting")) return;
|
|
27553
|
-
this.startRelayClient();
|
|
27591
|
+
await this.startRelayClient();
|
|
27554
27592
|
}
|
|
27555
27593
|
stop(options = {}) {
|
|
27556
27594
|
if (this.lifecycleState === "stopping" && this.stopPromise) return this.stopPromise;
|
|
@@ -27648,7 +27686,7 @@ var A2AServer = class {
|
|
|
27648
27686
|
const deadline = Date.now() + timeoutMs;
|
|
27649
27687
|
const remaining = () => Math.max(1, deadline - Date.now());
|
|
27650
27688
|
const relayClient = this.relayClient;
|
|
27651
|
-
this.relayClient = null;
|
|
27689
|
+
if (!this.externalRelayTransport) this.relayClient = null;
|
|
27652
27690
|
await relayClient?.stopAndWait(remaining());
|
|
27653
27691
|
const relayHub = this.relayHub;
|
|
27654
27692
|
this.relayHub = null;
|
|
@@ -27678,10 +27716,10 @@ var A2AServer = class {
|
|
|
27678
27716
|
};
|
|
27679
27717
|
|
|
27680
27718
|
// src/index.ts
|
|
27681
|
-
import { randomUUID as
|
|
27682
|
-
import { readFileSync as
|
|
27683
|
-
import { dirname, resolve as
|
|
27684
|
-
import { fileURLToPath } from "node:url";
|
|
27719
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
27720
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
27721
|
+
import { dirname as dirname2, resolve as resolve3 } from "node:path";
|
|
27722
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
27685
27723
|
|
|
27686
27724
|
// src/agentic-actions.ts
|
|
27687
27725
|
var FALLBACK_AGENT_ALIASES = [
|
|
@@ -28096,9 +28134,6 @@ ${replyResultText2}` : void 0,
|
|
|
28096
28134
|
return true;
|
|
28097
28135
|
}
|
|
28098
28136
|
|
|
28099
|
-
// src/chat-client.ts
|
|
28100
|
-
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
28101
|
-
|
|
28102
28137
|
// src/chat-store.ts
|
|
28103
28138
|
import { createHash } from "node:crypto";
|
|
28104
28139
|
import { createReadStream } from "node:fs";
|
|
@@ -28226,7 +28261,7 @@ var ChatStore = class {
|
|
|
28226
28261
|
let firstMessageAt = null;
|
|
28227
28262
|
let lastMessageAt = null;
|
|
28228
28263
|
let leftover = "";
|
|
28229
|
-
return new Promise((
|
|
28264
|
+
return new Promise((resolve4, reject) => {
|
|
28230
28265
|
stream.on("data", (chunk) => {
|
|
28231
28266
|
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
28232
28267
|
hash.update(text);
|
|
@@ -28266,7 +28301,7 @@ var ChatStore = class {
|
|
|
28266
28301
|
hash.update(metaRaw);
|
|
28267
28302
|
} catch {
|
|
28268
28303
|
}
|
|
28269
|
-
|
|
28304
|
+
resolve4({
|
|
28270
28305
|
transcriptHash: hash.digest("hex"),
|
|
28271
28306
|
hashAlgo: "sha256",
|
|
28272
28307
|
messageCount,
|
|
@@ -28301,7 +28336,439 @@ function isNotFound(err) {
|
|
|
28301
28336
|
return !!err && typeof err === "object" && err.code === "ENOENT";
|
|
28302
28337
|
}
|
|
28303
28338
|
|
|
28339
|
+
// src/openclaw-supervisor-client.ts
|
|
28340
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
28341
|
+
import { spawn } from "node:child_process";
|
|
28342
|
+
import { existsSync, readFileSync as readFileSync3, unlinkSync } from "node:fs";
|
|
28343
|
+
import { homedir as homedir3 } from "node:os";
|
|
28344
|
+
import net from "node:net";
|
|
28345
|
+
import { resolve } from "node:path";
|
|
28346
|
+
import { fileURLToPath } from "node:url";
|
|
28347
|
+
|
|
28348
|
+
// src/supervisor-protocol.ts
|
|
28349
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
28350
|
+
import { chmodSync, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "node:fs";
|
|
28351
|
+
import { tmpdir } from "node:os";
|
|
28352
|
+
import { dirname, join as join2 } from "node:path";
|
|
28353
|
+
var SUPERVISOR_PROTOCOL_VERSION = 2;
|
|
28354
|
+
var SUPERVISOR_RENEW_MS = 5e3;
|
|
28355
|
+
function supervisorOwnerHash(lifecycleKey) {
|
|
28356
|
+
return createHash2("sha256").update(lifecycleKey).digest("hex");
|
|
28357
|
+
}
|
|
28358
|
+
function supervisorDir(stateDir) {
|
|
28359
|
+
return join2(stateDir, "perkos-a2a", "supervisors");
|
|
28360
|
+
}
|
|
28361
|
+
function supervisorSocketPath(stateDir, ownerHash) {
|
|
28362
|
+
const preferred = join2(supervisorDir(stateDir), `${ownerHash.slice(0, 32)}.sock`);
|
|
28363
|
+
if (Buffer.byteLength(preferred) <= 96) return preferred;
|
|
28364
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : "user";
|
|
28365
|
+
return join2(tmpdir(), `perkos-a2a-${uid}`, `${ownerHash.slice(0, 32)}.sock`);
|
|
28366
|
+
}
|
|
28367
|
+
function supervisorBootstrapPath(stateDir, ownerHash) {
|
|
28368
|
+
return join2(supervisorDir(stateDir), `${ownerHash}.bootstrap.json`);
|
|
28369
|
+
}
|
|
28370
|
+
function currentBootId() {
|
|
28371
|
+
try {
|
|
28372
|
+
return readFileSync2("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
28373
|
+
} catch {
|
|
28374
|
+
return `process-boot-${Math.floor(Date.now() - process.uptime() * 1e3)}`;
|
|
28375
|
+
}
|
|
28376
|
+
}
|
|
28377
|
+
function processStartIdentity(pid = process.pid) {
|
|
28378
|
+
try {
|
|
28379
|
+
const stat = readFileSync2(`/proc/${pid}/stat`, "utf8");
|
|
28380
|
+
const afterName = stat.slice(stat.lastIndexOf(")") + 2).trim().split(/\s+/u);
|
|
28381
|
+
return `linux-start-ticks:${afterName[19] ?? "unknown"}`;
|
|
28382
|
+
} catch {
|
|
28383
|
+
if (pid === process.pid) return `epoch-ms:${Math.floor(Date.now() - process.uptime() * 1e3)}`;
|
|
28384
|
+
return "unknown";
|
|
28385
|
+
}
|
|
28386
|
+
}
|
|
28387
|
+
function encodeSupervisorMessage(message) {
|
|
28388
|
+
return `${JSON.stringify(message)}
|
|
28389
|
+
`;
|
|
28390
|
+
}
|
|
28391
|
+
|
|
28392
|
+
// src/openclaw-supervisor-client.ts
|
|
28393
|
+
var OpenClawSupervisorClient = class {
|
|
28394
|
+
stateDir;
|
|
28395
|
+
ownerHash;
|
|
28396
|
+
socketPath;
|
|
28397
|
+
config;
|
|
28398
|
+
logger;
|
|
28399
|
+
handlers;
|
|
28400
|
+
supervisorCliPath;
|
|
28401
|
+
lease;
|
|
28402
|
+
socket = null;
|
|
28403
|
+
startPromise = null;
|
|
28404
|
+
stopPromise = null;
|
|
28405
|
+
renewTimer = null;
|
|
28406
|
+
pending = /* @__PURE__ */ new Map();
|
|
28407
|
+
health = null;
|
|
28408
|
+
stopped = true;
|
|
28409
|
+
constructor(input) {
|
|
28410
|
+
this.stateDir = resolve(input.stateDir ?? process.env.OPENCLAW_STATE_DIR ?? `${homedir3()}/.openclaw`);
|
|
28411
|
+
this.ownerHash = input.ownerHash;
|
|
28412
|
+
this.socketPath = supervisorSocketPath(this.stateDir, this.ownerHash);
|
|
28413
|
+
this.config = input.config;
|
|
28414
|
+
this.logger = input.logger;
|
|
28415
|
+
this.handlers = input.handlers;
|
|
28416
|
+
this.supervisorCliPath = input.supervisorCliPath;
|
|
28417
|
+
this.lease = {
|
|
28418
|
+
ownerKey: input.ownerHash,
|
|
28419
|
+
instanceId: input.instanceId,
|
|
28420
|
+
generation: input.generation,
|
|
28421
|
+
parentPid: process.pid,
|
|
28422
|
+
parentStartTime: processStartIdentity(),
|
|
28423
|
+
bootId: currentBootId(),
|
|
28424
|
+
lastRenewedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
28425
|
+
};
|
|
28426
|
+
}
|
|
28427
|
+
isConnected() {
|
|
28428
|
+
return this.health?.relayConnected === true && this.health?.relayRegistered === true;
|
|
28429
|
+
}
|
|
28430
|
+
isChatConnected() {
|
|
28431
|
+
return this.health?.chatConnected === true;
|
|
28432
|
+
}
|
|
28433
|
+
currentHealth() {
|
|
28434
|
+
return this.health;
|
|
28435
|
+
}
|
|
28436
|
+
start() {
|
|
28437
|
+
if (!this.stopped && this.socket && !this.socket.destroyed) return Promise.resolve();
|
|
28438
|
+
if (this.startPromise) return this.startPromise;
|
|
28439
|
+
this.stopped = false;
|
|
28440
|
+
const starting = (async () => {
|
|
28441
|
+
await this.connectOrSpawn();
|
|
28442
|
+
const claim = await this.requestWithMessage({
|
|
28443
|
+
protocolVersion: 2,
|
|
28444
|
+
type: "claim",
|
|
28445
|
+
requestId: randomUUID5(),
|
|
28446
|
+
lease: this.freshLease(),
|
|
28447
|
+
config: this.config
|
|
28448
|
+
}, 15e3);
|
|
28449
|
+
const health = claim?.health;
|
|
28450
|
+
if (claim?.state !== "READY" || health?.schemaVersion !== 2 || health.authority !== "process-external-supervisor" || health.state !== "running" || health.ownerKey !== this.ownerHash || health.instanceId !== this.lease.instanceId || health.generation !== this.lease.generation || health.relayClientCount !== (this.config.relay?.enabled && this.config.relay.url ? 1 : 0) || !health.claimedAt) {
|
|
28451
|
+
throw new Error("OWNERSHIP_PRIMITIVE_UNAVAILABLE:supervisor claim did not return certified READY/CLAIMED health");
|
|
28452
|
+
}
|
|
28453
|
+
this.health = health;
|
|
28454
|
+
this.renewTimer = setInterval(() => this.renew(), SUPERVISOR_RENEW_MS);
|
|
28455
|
+
this.renewTimer.unref?.();
|
|
28456
|
+
this.logger.info(`[perkos-a2a] Supervisor owner ready generation=${this.lease.generation} instanceId=${this.lease.instanceId}`);
|
|
28457
|
+
})().finally(() => {
|
|
28458
|
+
if (this.startPromise === starting) this.startPromise = null;
|
|
28459
|
+
});
|
|
28460
|
+
this.startPromise = starting;
|
|
28461
|
+
return starting;
|
|
28462
|
+
}
|
|
28463
|
+
stop() {
|
|
28464
|
+
if (this.stopPromise) return this.stopPromise;
|
|
28465
|
+
if (this.stopped && !this.socket) return Promise.resolve();
|
|
28466
|
+
this.stopped = true;
|
|
28467
|
+
if (this.renewTimer) clearInterval(this.renewTimer);
|
|
28468
|
+
this.renewTimer = null;
|
|
28469
|
+
const stopping = (async () => {
|
|
28470
|
+
if (this.socket && !this.socket.destroyed) {
|
|
28471
|
+
const requestId = randomUUID5();
|
|
28472
|
+
try {
|
|
28473
|
+
await this.requestWithMessage({
|
|
28474
|
+
protocolVersion: 2,
|
|
28475
|
+
type: "release",
|
|
28476
|
+
requestId,
|
|
28477
|
+
lease: this.freshLease()
|
|
28478
|
+
}, 8e3);
|
|
28479
|
+
} catch (error) {
|
|
28480
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
28481
|
+
if (!message.includes("NOT_OWNER") && !message.includes("SUPERVISOR_DISCONNECTED")) throw error;
|
|
28482
|
+
}
|
|
28483
|
+
}
|
|
28484
|
+
this.closeSocket(new Error("CLIENT_STOPPED"));
|
|
28485
|
+
this.logger.info(`[perkos-a2a] A2A server stopped generation=${this.lease.generation} instanceId=${this.lease.instanceId}`);
|
|
28486
|
+
})().finally(() => {
|
|
28487
|
+
if (this.stopPromise === stopping) this.stopPromise = null;
|
|
28488
|
+
});
|
|
28489
|
+
this.stopPromise = stopping;
|
|
28490
|
+
return stopping;
|
|
28491
|
+
}
|
|
28492
|
+
async stopAndWait() {
|
|
28493
|
+
await this.stop();
|
|
28494
|
+
return { closeTimedOut: false };
|
|
28495
|
+
}
|
|
28496
|
+
async sendTask(targetAgent, payload, taskId) {
|
|
28497
|
+
return await this.request("sendTask", { target: targetAgent, payload, ...taskId ? { taskId } : {} }, 7e4);
|
|
28498
|
+
}
|
|
28499
|
+
async discover() {
|
|
28500
|
+
return await this.request("discover", {}, 15e3);
|
|
28501
|
+
}
|
|
28502
|
+
sendTaskResponse(original, result) {
|
|
28503
|
+
this.write({ protocolVersion: 2, type: "relayTaskResult", lease: this.freshLease(), eventId: randomUUID5(), original, result });
|
|
28504
|
+
}
|
|
28505
|
+
async sendChatReply(payload) {
|
|
28506
|
+
return await this.request("chatReply", payload, 15e3);
|
|
28507
|
+
}
|
|
28508
|
+
async request(method, payload, timeoutMs) {
|
|
28509
|
+
const requestId = randomUUID5();
|
|
28510
|
+
return this.requestWithMessage({ protocolVersion: 2, type: "request", requestId, lease: this.freshLease(), method, payload }, timeoutMs);
|
|
28511
|
+
}
|
|
28512
|
+
async requestWithMessage(message, timeoutMs) {
|
|
28513
|
+
return new Promise((resolveRequest, rejectRequest) => {
|
|
28514
|
+
const timer = setTimeout(() => {
|
|
28515
|
+
this.pending.delete(message.requestId);
|
|
28516
|
+
rejectRequest(new Error(`SUPERVISOR_REQUEST_TIMEOUT:${message.type}`));
|
|
28517
|
+
}, timeoutMs);
|
|
28518
|
+
timer.unref?.();
|
|
28519
|
+
this.pending.set(message.requestId, { resolve: resolveRequest, reject: rejectRequest, timer });
|
|
28520
|
+
try {
|
|
28521
|
+
this.write(message);
|
|
28522
|
+
} catch (error) {
|
|
28523
|
+
clearTimeout(timer);
|
|
28524
|
+
this.pending.delete(message.requestId);
|
|
28525
|
+
rejectRequest(error instanceof Error ? error : new Error(String(error)));
|
|
28526
|
+
}
|
|
28527
|
+
});
|
|
28528
|
+
}
|
|
28529
|
+
freshLease() {
|
|
28530
|
+
this.lease.lastRenewedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
28531
|
+
return { ...this.lease };
|
|
28532
|
+
}
|
|
28533
|
+
renew() {
|
|
28534
|
+
if (this.stopped || !this.socket || this.socket.destroyed) return;
|
|
28535
|
+
this.write({ protocolVersion: 2, type: "renew", lease: this.freshLease() });
|
|
28536
|
+
}
|
|
28537
|
+
async connectOrSpawn() {
|
|
28538
|
+
try {
|
|
28539
|
+
await this.connectSocket(1e3);
|
|
28540
|
+
return;
|
|
28541
|
+
} catch {
|
|
28542
|
+
}
|
|
28543
|
+
const cliPath = this.supervisorCliPath ?? fileURLToPath(new URL("./openclaw-supervisor-cli.js", import.meta.url));
|
|
28544
|
+
if (!existsSync(cliPath)) {
|
|
28545
|
+
throw new Error(`OWNERSHIP_PRIMITIVE_UNAVAILABLE:supervisor CLI is missing at ${cliPath}`);
|
|
28546
|
+
}
|
|
28547
|
+
const bootstrapPath = supervisorBootstrapPath(this.stateDir, this.ownerHash);
|
|
28548
|
+
try {
|
|
28549
|
+
unlinkSync(bootstrapPath);
|
|
28550
|
+
} catch {
|
|
28551
|
+
}
|
|
28552
|
+
let childExit;
|
|
28553
|
+
const child = spawn(process.execPath, [cliPath, "--state-dir", this.stateDir, "--owner-key", this.ownerHash], {
|
|
28554
|
+
detached: true,
|
|
28555
|
+
stdio: "ignore",
|
|
28556
|
+
env: { ...process.env }
|
|
28557
|
+
});
|
|
28558
|
+
child.once("error", (error) => {
|
|
28559
|
+
childExit = `spawn error: ${error.message}`;
|
|
28560
|
+
});
|
|
28561
|
+
child.once("exit", (code, signal) => {
|
|
28562
|
+
childExit = `exit code=${code ?? "null"} signal=${signal ?? "none"}`;
|
|
28563
|
+
});
|
|
28564
|
+
child.unref();
|
|
28565
|
+
const deadline = Date.now() + 1e4;
|
|
28566
|
+
let lastError;
|
|
28567
|
+
while (Date.now() < deadline) {
|
|
28568
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 100));
|
|
28569
|
+
try {
|
|
28570
|
+
await this.connectSocket(1e3);
|
|
28571
|
+
return;
|
|
28572
|
+
} catch (error) {
|
|
28573
|
+
lastError = error;
|
|
28574
|
+
}
|
|
28575
|
+
const bootstrap = this.readBootstrapStatus(bootstrapPath);
|
|
28576
|
+
if (bootstrap?.state === "failed") {
|
|
28577
|
+
throw new Error(`OWNERSHIP_PRIMITIVE_UNAVAILABLE:${bootstrap.errorCode || "SUPERVISOR_BOOT_FAILED"}:${bootstrap.message || "supervisor failed before READY"}`);
|
|
28578
|
+
}
|
|
28579
|
+
if (childExit) throw new Error(`OWNERSHIP_PRIMITIVE_UNAVAILABLE:supervisor ${childExit}`);
|
|
28580
|
+
}
|
|
28581
|
+
throw new Error(`OWNERSHIP_PRIMITIVE_UNAVAILABLE:${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
|
28582
|
+
}
|
|
28583
|
+
readBootstrapStatus(path) {
|
|
28584
|
+
try {
|
|
28585
|
+
const value = JSON.parse(readFileSync3(path, "utf8"));
|
|
28586
|
+
return value?.schemaVersion === 1 && value.ownerKey === this.ownerHash ? value : null;
|
|
28587
|
+
} catch {
|
|
28588
|
+
return null;
|
|
28589
|
+
}
|
|
28590
|
+
}
|
|
28591
|
+
connectSocket(timeoutMs) {
|
|
28592
|
+
this.closeSocket(new Error("SUPERVISOR_RECONNECT"));
|
|
28593
|
+
return new Promise((resolveConnect, rejectConnect) => {
|
|
28594
|
+
const socket = net.createConnection(this.socketPath);
|
|
28595
|
+
socket.setEncoding("utf8");
|
|
28596
|
+
let buffered = "";
|
|
28597
|
+
let settled = false;
|
|
28598
|
+
const timer = setTimeout(() => {
|
|
28599
|
+
if (settled) return;
|
|
28600
|
+
settled = true;
|
|
28601
|
+
socket.destroy();
|
|
28602
|
+
rejectConnect(new Error("supervisor socket connect timeout"));
|
|
28603
|
+
}, timeoutMs);
|
|
28604
|
+
timer.unref?.();
|
|
28605
|
+
socket.once("connect", () => {
|
|
28606
|
+
if (settled) return;
|
|
28607
|
+
settled = true;
|
|
28608
|
+
clearTimeout(timer);
|
|
28609
|
+
this.socket = socket;
|
|
28610
|
+
resolveConnect();
|
|
28611
|
+
});
|
|
28612
|
+
socket.once("error", (error) => {
|
|
28613
|
+
if (!settled) {
|
|
28614
|
+
settled = true;
|
|
28615
|
+
clearTimeout(timer);
|
|
28616
|
+
rejectConnect(error);
|
|
28617
|
+
}
|
|
28618
|
+
});
|
|
28619
|
+
socket.on("data", (chunk) => {
|
|
28620
|
+
buffered += chunk;
|
|
28621
|
+
for (; ; ) {
|
|
28622
|
+
const newline = buffered.indexOf("\n");
|
|
28623
|
+
if (newline < 0) break;
|
|
28624
|
+
const line = buffered.slice(0, newline);
|
|
28625
|
+
buffered = buffered.slice(newline + 1);
|
|
28626
|
+
if (line.trim()) void this.handleEvent(JSON.parse(line));
|
|
28627
|
+
}
|
|
28628
|
+
});
|
|
28629
|
+
socket.on("close", () => {
|
|
28630
|
+
if (this.socket === socket) this.closeSocket(new Error("SUPERVISOR_DISCONNECTED"));
|
|
28631
|
+
});
|
|
28632
|
+
});
|
|
28633
|
+
}
|
|
28634
|
+
async handleEvent(event) {
|
|
28635
|
+
if (event.protocolVersion !== SUPERVISOR_PROTOCOL_VERSION) return;
|
|
28636
|
+
if (event.type === "response") {
|
|
28637
|
+
const pending = this.pending.get(event.requestId);
|
|
28638
|
+
if (!pending) return;
|
|
28639
|
+
this.pending.delete(event.requestId);
|
|
28640
|
+
clearTimeout(pending.timer);
|
|
28641
|
+
if (event.ok) pending.resolve(event.result);
|
|
28642
|
+
else pending.reject(new Error(`${event.error?.code || "SUPERVISOR_ERROR"}:${event.error?.message || "request failed"}`));
|
|
28643
|
+
return;
|
|
28644
|
+
}
|
|
28645
|
+
if (event.type === "health") {
|
|
28646
|
+
this.health = event.health;
|
|
28647
|
+
return;
|
|
28648
|
+
}
|
|
28649
|
+
if (event.type === "log") {
|
|
28650
|
+
(event.level === "error" ? this.logger.error : this.logger.info)(event.message);
|
|
28651
|
+
return;
|
|
28652
|
+
}
|
|
28653
|
+
if (event.type === "relayTask") {
|
|
28654
|
+
try {
|
|
28655
|
+
const result = await this.handlers.onRelayTask(event.message);
|
|
28656
|
+
this.write({ protocolVersion: 2, type: "relayTaskResult", lease: this.freshLease(), eventId: event.eventId, original: event.message, result });
|
|
28657
|
+
} catch (error) {
|
|
28658
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
28659
|
+
this.write({
|
|
28660
|
+
protocolVersion: 2,
|
|
28661
|
+
type: "relayTaskResult",
|
|
28662
|
+
lease: this.freshLease(),
|
|
28663
|
+
eventId: event.eventId,
|
|
28664
|
+
original: event.message,
|
|
28665
|
+
result: { jsonrpc: "2.0", id: event.message.id, error: { code: -32603, message } }
|
|
28666
|
+
});
|
|
28667
|
+
}
|
|
28668
|
+
return;
|
|
28669
|
+
}
|
|
28670
|
+
if (event.type === "chatDeliver") await this.handlers.onChatDeliver(event.frame);
|
|
28671
|
+
if (event.type === "channelJoin") await this.handlers.onChannelJoin(event.frame);
|
|
28672
|
+
}
|
|
28673
|
+
write(message) {
|
|
28674
|
+
if (!this.socket || this.socket.destroyed) throw new Error("SUPERVISOR_DISCONNECTED");
|
|
28675
|
+
this.socket.write(encodeSupervisorMessage(message));
|
|
28676
|
+
}
|
|
28677
|
+
closeSocket(error) {
|
|
28678
|
+
if (this.renewTimer) clearInterval(this.renewTimer);
|
|
28679
|
+
this.renewTimer = null;
|
|
28680
|
+
const socket = this.socket;
|
|
28681
|
+
this.socket = null;
|
|
28682
|
+
if (socket && !socket.destroyed) socket.destroy();
|
|
28683
|
+
for (const pending of this.pending.values()) {
|
|
28684
|
+
clearTimeout(pending.timer);
|
|
28685
|
+
pending.reject(error);
|
|
28686
|
+
}
|
|
28687
|
+
this.pending.clear();
|
|
28688
|
+
}
|
|
28689
|
+
};
|
|
28690
|
+
|
|
28691
|
+
// src/runtime-evidence.ts
|
|
28692
|
+
import { createHmac } from "node:crypto";
|
|
28693
|
+
import {
|
|
28694
|
+
chmodSync as chmodSync2,
|
|
28695
|
+
mkdirSync as mkdirSync2,
|
|
28696
|
+
readFileSync as readFileSync4,
|
|
28697
|
+
renameSync as renameSync2,
|
|
28698
|
+
writeFileSync as writeFileSync2
|
|
28699
|
+
} from "node:fs";
|
|
28700
|
+
import { homedir as homedir4 } from "node:os";
|
|
28701
|
+
import { join as join3, resolve as resolve2 } from "node:path";
|
|
28702
|
+
var RUNTIME_EVIDENCE_SCHEMA_VERSION = 2;
|
|
28703
|
+
function canonicalize(value) {
|
|
28704
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
28705
|
+
if (!value || typeof value !== "object") return value;
|
|
28706
|
+
return Object.fromEntries(
|
|
28707
|
+
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, child]) => [key, canonicalize(child)])
|
|
28708
|
+
);
|
|
28709
|
+
}
|
|
28710
|
+
function canonicalConfigJson(config) {
|
|
28711
|
+
return JSON.stringify({ schemaVersion: RUNTIME_EVIDENCE_SCHEMA_VERSION, config: canonicalize(config) });
|
|
28712
|
+
}
|
|
28713
|
+
function configFingerprint(config, hmacKeyHex) {
|
|
28714
|
+
if (!/^[a-f0-9]{64}$/u.test(hmacKeyHex)) throw new Error("runtime evidence HMAC key is invalid");
|
|
28715
|
+
return createHmac("sha256", Buffer.from(hmacKeyHex, "hex")).update(canonicalConfigJson(config)).digest("hex");
|
|
28716
|
+
}
|
|
28717
|
+
function runtimeEvidenceDir(stateDir) {
|
|
28718
|
+
return resolve2(stateDir ?? process.env.OPENCLAW_STATE_DIR ?? join3(homedir4(), ".openclaw"), "perkos-a2a");
|
|
28719
|
+
}
|
|
28720
|
+
function runtimeAttemptPath(stateDir) {
|
|
28721
|
+
return join3(runtimeEvidenceDir(stateDir), "active-attempt.json");
|
|
28722
|
+
}
|
|
28723
|
+
function runtimeStatusPath(stateDir) {
|
|
28724
|
+
return join3(runtimeEvidenceDir(stateDir), "runtime-status.json");
|
|
28725
|
+
}
|
|
28726
|
+
function configAgentId(config) {
|
|
28727
|
+
if (config.platform?.agentId) return config.platform.agentId;
|
|
28728
|
+
const match = config.platform?.heartbeatUrl?.match(/\/agents\/([^/]+)\/heartbeat/u);
|
|
28729
|
+
return match?.[1] ?? "unknown";
|
|
28730
|
+
}
|
|
28731
|
+
function recordRuntimeLoadEvidence(input) {
|
|
28732
|
+
const evidenceDir = runtimeEvidenceDir(input.stateDir);
|
|
28733
|
+
let attempt;
|
|
28734
|
+
try {
|
|
28735
|
+
attempt = JSON.parse(readFileSync4(runtimeAttemptPath(input.stateDir), "utf8"));
|
|
28736
|
+
} catch {
|
|
28737
|
+
input.logger.info(`[perkos-a2a] config loaded version=${input.version} agentId=${configAgentId(input.config)} attemptId=none`);
|
|
28738
|
+
return null;
|
|
28739
|
+
}
|
|
28740
|
+
const agentId = configAgentId(input.config);
|
|
28741
|
+
if (attempt.schemaVersion !== RUNTIME_EVIDENCE_SCHEMA_VERSION || attempt.targetVersion !== input.version || attempt.expectedAgentId !== agentId) {
|
|
28742
|
+
input.logger.warn?.("[perkos-a2a] runtime evidence attempt does not match the loaded plugin identity");
|
|
28743
|
+
return null;
|
|
28744
|
+
}
|
|
28745
|
+
const status = {
|
|
28746
|
+
schemaVersion: RUNTIME_EVIDENCE_SCHEMA_VERSION,
|
|
28747
|
+
loadedAt: (input.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
28748
|
+
version: input.version,
|
|
28749
|
+
agentId,
|
|
28750
|
+
fingerprint: configFingerprint(input.config, attempt.hmacKeyHex),
|
|
28751
|
+
attemptId: attempt.attemptId,
|
|
28752
|
+
pid: process.pid,
|
|
28753
|
+
processStartTime: processStartIdentity(),
|
|
28754
|
+
bootId: currentBootId(),
|
|
28755
|
+
pluginRoot: resolve2(input.pluginRoot)
|
|
28756
|
+
};
|
|
28757
|
+
mkdirSync2(evidenceDir, { recursive: true, mode: 448 });
|
|
28758
|
+
const target = runtimeStatusPath(input.stateDir);
|
|
28759
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
28760
|
+
writeFileSync2(temporary, `${JSON.stringify(status, null, 2)}
|
|
28761
|
+
`, { mode: 384 });
|
|
28762
|
+
renameSync2(temporary, target);
|
|
28763
|
+
chmodSync2(target, 384);
|
|
28764
|
+
input.logger.info(
|
|
28765
|
+
`[perkos-a2a] config loaded version=${status.version} agentId=${status.agentId} attemptId=${status.attemptId} fingerprint=${status.fingerprint}`
|
|
28766
|
+
);
|
|
28767
|
+
return status;
|
|
28768
|
+
}
|
|
28769
|
+
|
|
28304
28770
|
// src/chat-client.ts
|
|
28771
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
28305
28772
|
var DEFAULT_URL = "wss://chat.perkos.xyz/chat";
|
|
28306
28773
|
var DEFAULT_HISTORY_LIMIT = 50;
|
|
28307
28774
|
var DEFAULT_MIN_RECONNECT = 1e3;
|
|
@@ -28324,6 +28791,7 @@ var ChatClient = class {
|
|
|
28324
28791
|
connected = false;
|
|
28325
28792
|
authed = false;
|
|
28326
28793
|
stopped = false;
|
|
28794
|
+
generation = 0;
|
|
28327
28795
|
constructor(opts) {
|
|
28328
28796
|
this.agentName = opts.agentName;
|
|
28329
28797
|
const cfg = opts.config;
|
|
@@ -28359,20 +28827,43 @@ var ChatClient = class {
|
|
|
28359
28827
|
return;
|
|
28360
28828
|
}
|
|
28361
28829
|
this.stopped = false;
|
|
28362
|
-
this.
|
|
28830
|
+
const generation = ++this.generation;
|
|
28831
|
+
this.connect(generation);
|
|
28363
28832
|
}
|
|
28364
28833
|
stop() {
|
|
28365
|
-
|
|
28366
|
-
|
|
28367
|
-
|
|
28834
|
+
const socket = this.beginStop();
|
|
28835
|
+
try {
|
|
28836
|
+
socket?.close(1e3, "client shutting down");
|
|
28837
|
+
} catch {
|
|
28838
|
+
}
|
|
28839
|
+
}
|
|
28840
|
+
async stopAndWait(timeoutMs = 5e3) {
|
|
28841
|
+
const socket = this.beginStop();
|
|
28842
|
+
if (!socket || socket.readyState === wrapper_default.CLOSED) return { closeTimedOut: false };
|
|
28843
|
+
return new Promise((resolve4) => {
|
|
28844
|
+
let settled = false;
|
|
28845
|
+
const finish = (closeTimedOut) => {
|
|
28846
|
+
if (settled) return;
|
|
28847
|
+
settled = true;
|
|
28848
|
+
clearTimeout(timer);
|
|
28849
|
+
resolve4({ closeTimedOut });
|
|
28850
|
+
};
|
|
28851
|
+
const timer = setTimeout(() => {
|
|
28852
|
+
try {
|
|
28853
|
+
socket.terminate();
|
|
28854
|
+
} catch {
|
|
28855
|
+
}
|
|
28856
|
+
finish(true);
|
|
28857
|
+
}, timeoutMs);
|
|
28858
|
+
timer.unref?.();
|
|
28859
|
+
socket.once("close", () => finish(false));
|
|
28860
|
+
socket.once("error", () => finish(false));
|
|
28368
28861
|
try {
|
|
28369
|
-
|
|
28862
|
+
socket.close(1e3, "client shutting down");
|
|
28370
28863
|
} catch {
|
|
28864
|
+
finish(false);
|
|
28371
28865
|
}
|
|
28372
|
-
|
|
28373
|
-
}
|
|
28374
|
-
this.connected = false;
|
|
28375
|
-
this.authed = false;
|
|
28866
|
+
});
|
|
28376
28867
|
}
|
|
28377
28868
|
/**
|
|
28378
28869
|
* Send a reply from this agent into a conversation. Appends to the local
|
|
@@ -28382,7 +28873,7 @@ var ChatClient = class {
|
|
|
28382
28873
|
* required (the simple agent loop should just regenerate from history).
|
|
28383
28874
|
*/
|
|
28384
28875
|
async sendReply(opts) {
|
|
28385
|
-
const id =
|
|
28876
|
+
const id = randomUUID6();
|
|
28386
28877
|
const msg = {
|
|
28387
28878
|
id,
|
|
28388
28879
|
from: `agent:${this.agentName}`,
|
|
@@ -28405,27 +28896,32 @@ var ChatClient = class {
|
|
|
28405
28896
|
// -------------------------------------------------------------------------
|
|
28406
28897
|
// WebSocket lifecycle
|
|
28407
28898
|
// -------------------------------------------------------------------------
|
|
28408
|
-
connect() {
|
|
28409
|
-
if (this.
|
|
28899
|
+
connect(generation) {
|
|
28900
|
+
if (!this.isCurrent(generation)) return;
|
|
28410
28901
|
const url = this.config.url;
|
|
28411
28902
|
this.logger.info(`[perkos-chat] connecting to ${url}`);
|
|
28412
28903
|
try {
|
|
28413
28904
|
this.ws = new wrapper_default(url);
|
|
28414
28905
|
} catch (err) {
|
|
28415
28906
|
this.logger.error(`[perkos-chat] failed to construct WS: ${errMsg(err)}`);
|
|
28416
|
-
this.scheduleReconnect();
|
|
28907
|
+
this.scheduleReconnect(generation);
|
|
28417
28908
|
return;
|
|
28418
28909
|
}
|
|
28910
|
+
const socket = this.ws;
|
|
28419
28911
|
let authTimer = setTimeout(() => {
|
|
28420
|
-
if (!this.authed) {
|
|
28912
|
+
if (this.isCurrent(generation, socket) && !this.authed) {
|
|
28421
28913
|
this.logger.error("[perkos-chat] auth timeout");
|
|
28422
28914
|
try {
|
|
28423
|
-
|
|
28915
|
+
socket.close(4002, "auth timeout");
|
|
28424
28916
|
} catch {
|
|
28425
28917
|
}
|
|
28426
28918
|
}
|
|
28427
28919
|
}, AUTH_TIMEOUT_MS);
|
|
28428
28920
|
this.ws.on("open", () => {
|
|
28921
|
+
if (!this.isCurrent(generation, socket)) {
|
|
28922
|
+
socket.close(1e3, "stale chat generation");
|
|
28923
|
+
return;
|
|
28924
|
+
}
|
|
28429
28925
|
this.connected = true;
|
|
28430
28926
|
this.reconnectMs = this.config.minReconnectMs;
|
|
28431
28927
|
this.sendFrame({
|
|
@@ -28436,6 +28932,7 @@ var ChatClient = class {
|
|
|
28436
28932
|
});
|
|
28437
28933
|
});
|
|
28438
28934
|
this.ws.on("message", (data) => {
|
|
28935
|
+
if (!this.isCurrent(generation, socket)) return;
|
|
28439
28936
|
let frame;
|
|
28440
28937
|
try {
|
|
28441
28938
|
frame = JSON.parse(data.toString("utf8"));
|
|
@@ -28450,19 +28947,21 @@ var ChatClient = class {
|
|
|
28450
28947
|
void this.handleFrame(frame);
|
|
28451
28948
|
});
|
|
28452
28949
|
this.ws.on("close", (code, reason) => {
|
|
28453
|
-
this.connected = false;
|
|
28454
|
-
this.authed = false;
|
|
28455
|
-
this.clearTimers();
|
|
28456
28950
|
if (authTimer) {
|
|
28457
28951
|
clearTimeout(authTimer);
|
|
28458
28952
|
authTimer = null;
|
|
28459
28953
|
}
|
|
28954
|
+
if (!this.isCurrent(generation, socket)) return;
|
|
28955
|
+
this.connected = false;
|
|
28956
|
+
this.authed = false;
|
|
28957
|
+
this.clearTimers();
|
|
28460
28958
|
if (!this.stopped) {
|
|
28461
28959
|
this.logger.info(`[perkos-chat] disconnected (${code}: ${reason?.toString() || "no reason"}); reconnecting`);
|
|
28462
|
-
this.scheduleReconnect();
|
|
28960
|
+
this.scheduleReconnect(generation);
|
|
28463
28961
|
}
|
|
28464
28962
|
});
|
|
28465
28963
|
this.ws.on("error", (err) => {
|
|
28964
|
+
if (!this.isCurrent(generation, socket)) return;
|
|
28466
28965
|
this.logger.error(`[perkos-chat] WS error: ${errMsg(err)}`);
|
|
28467
28966
|
});
|
|
28468
28967
|
}
|
|
@@ -28651,10 +29150,12 @@ var ChatClient = class {
|
|
|
28651
29150
|
}
|
|
28652
29151
|
}, this.config.heartbeatIntervalMs);
|
|
28653
29152
|
}
|
|
28654
|
-
scheduleReconnect() {
|
|
28655
|
-
if (this.
|
|
29153
|
+
scheduleReconnect(generation) {
|
|
29154
|
+
if (!this.isCurrent(generation)) return;
|
|
28656
29155
|
const delay = this.reconnectMs;
|
|
28657
|
-
this.reconnectTimer = setTimeout(() =>
|
|
29156
|
+
this.reconnectTimer = setTimeout(() => {
|
|
29157
|
+
if (this.isCurrent(generation)) this.connect(generation);
|
|
29158
|
+
}, delay);
|
|
28658
29159
|
this.reconnectMs = Math.min(delay * 2, this.config.maxReconnectMs);
|
|
28659
29160
|
}
|
|
28660
29161
|
clearTimers() {
|
|
@@ -28667,151 +29168,25 @@ var ChatClient = class {
|
|
|
28667
29168
|
this.reconnectTimer = null;
|
|
28668
29169
|
}
|
|
28669
29170
|
}
|
|
29171
|
+
isCurrent(generation, socket) {
|
|
29172
|
+
return !this.stopped && generation === this.generation && (!socket || socket === this.ws);
|
|
29173
|
+
}
|
|
29174
|
+
beginStop() {
|
|
29175
|
+
if (this.stopped && !this.ws) return null;
|
|
29176
|
+
this.stopped = true;
|
|
29177
|
+
this.generation += 1;
|
|
29178
|
+
this.clearTimers();
|
|
29179
|
+
const socket = this.ws;
|
|
29180
|
+
this.ws = null;
|
|
29181
|
+
this.connected = false;
|
|
29182
|
+
this.authed = false;
|
|
29183
|
+
return socket;
|
|
29184
|
+
}
|
|
28670
29185
|
};
|
|
28671
29186
|
function errMsg(err) {
|
|
28672
29187
|
return err instanceof Error ? err.message : String(err);
|
|
28673
29188
|
}
|
|
28674
29189
|
|
|
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
29190
|
// src/types.ts
|
|
28816
29191
|
function isWebhookEvent(msg) {
|
|
28817
29192
|
return msg.type === "webhook_event";
|
|
@@ -28820,7 +29195,7 @@ function isWebhookEvent(msg) {
|
|
|
28820
29195
|
// src/index.ts
|
|
28821
29196
|
function loadedPluginVersion() {
|
|
28822
29197
|
try {
|
|
28823
|
-
const manifest = JSON.parse(
|
|
29198
|
+
const manifest = JSON.parse(readFileSync5(new URL("../openclaw.plugin.json", import.meta.url), "utf8"));
|
|
28824
29199
|
return typeof manifest.version === "string" ? manifest.version : "unknown";
|
|
28825
29200
|
} catch {
|
|
28826
29201
|
return "unknown";
|
|
@@ -28915,7 +29290,7 @@ async function prepareOpenClawChatSession(api, config, sessionKey, logger) {
|
|
|
28915
29290
|
try {
|
|
28916
29291
|
await patchSessionEntry({
|
|
28917
29292
|
sessionKey,
|
|
28918
|
-
fallbackEntry: { sessionId:
|
|
29293
|
+
fallbackEntry: { sessionId: randomUUID7(), updatedAt: Date.now() },
|
|
28919
29294
|
preserveActivity: true,
|
|
28920
29295
|
replaceEntry: true,
|
|
28921
29296
|
update: (entry) => alignOpenClawChatSessionModel(entry, config.runtime?.model)
|
|
@@ -28935,7 +29310,7 @@ async function forceNativeOpenClawChatRuntime(api, config, sessionKey, logger) {
|
|
|
28935
29310
|
try {
|
|
28936
29311
|
await patchSessionEntry({
|
|
28937
29312
|
sessionKey,
|
|
28938
|
-
fallbackEntry: { sessionId:
|
|
29313
|
+
fallbackEntry: { sessionId: randomUUID7(), updatedAt: Date.now() },
|
|
28939
29314
|
preserveActivity: true,
|
|
28940
29315
|
replaceEntry: true,
|
|
28941
29316
|
update: (entry) => ({
|
|
@@ -28976,7 +29351,7 @@ async function runOpenClawChatTurn(api, config, sessionKey, prompt, transcriptPr
|
|
|
28976
29351
|
const runtimeConfig = currentConfig();
|
|
28977
29352
|
const workspaceDir = resolveAgentWorkspaceDir(runtimeConfig, agentId);
|
|
28978
29353
|
const entry = api.runtime?.agent?.session?.getSessionEntry?.({ agentId, sessionKey });
|
|
28979
|
-
const sessionId = typeof entry?.sessionId === "string" && entry.sessionId.trim() ? entry.sessionId :
|
|
29354
|
+
const sessionId = typeof entry?.sessionId === "string" && entry.sessionId.trim() ? entry.sessionId : randomUUID7();
|
|
28980
29355
|
const configuredTimeout = api.runtime?.agent?.resolveAgentTimeoutMs?.(runtimeConfig);
|
|
28981
29356
|
const timeoutMs = Math.min(
|
|
28982
29357
|
75e3,
|
|
@@ -28991,7 +29366,7 @@ async function runOpenClawChatTurn(api, config, sessionKey, prompt, transcriptPr
|
|
|
28991
29366
|
prompt,
|
|
28992
29367
|
transcriptPrompt,
|
|
28993
29368
|
timeoutMs,
|
|
28994
|
-
runId:
|
|
29369
|
+
runId: randomUUID7(),
|
|
28995
29370
|
trigger: "user",
|
|
28996
29371
|
messageChannel: "perkos-chat",
|
|
28997
29372
|
disableTools: true,
|
|
@@ -29077,11 +29452,6 @@ async function deliverToHermes(config, message, logger) {
|
|
|
29077
29452
|
}
|
|
29078
29453
|
logger.info(`[perkos-a2a] Delivered inbound A2A task to Hermes API session ${sessionKey} via ${url}`);
|
|
29079
29454
|
}
|
|
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
29455
|
function a2aLifecycleKey(config) {
|
|
29086
29456
|
const rawRelayUrl = (config.relay?.url || "no-relay").trim();
|
|
29087
29457
|
let relayUrl = rawRelayUrl.replace(/[?#].*$/u, "").replace(/\/+$/u, "").toLowerCase();
|
|
@@ -29111,12 +29481,12 @@ function register(api) {
|
|
|
29111
29481
|
recordRuntimeLoadEvidence({
|
|
29112
29482
|
config: pluginConfig,
|
|
29113
29483
|
version: pluginVersion,
|
|
29114
|
-
pluginRoot:
|
|
29484
|
+
pluginRoot: resolve3(dirname2(fileURLToPath2(import.meta.url)), ".."),
|
|
29115
29485
|
logger
|
|
29116
29486
|
});
|
|
29117
|
-
const server = new A2AServer(pluginConfig, logger);
|
|
29487
|
+
const server = new A2AServer(pluginConfig, logger, { relayOwnership: "external-required" });
|
|
29118
29488
|
const lifecycleKey = a2aLifecycleKey(pluginConfig);
|
|
29119
|
-
const lifecycleInstanceId =
|
|
29489
|
+
const lifecycleInstanceId = randomUUID7();
|
|
29120
29490
|
const pendingTasks = [];
|
|
29121
29491
|
const enqueueSystemEvent = api.runtime?.system?.enqueueSystemEvent;
|
|
29122
29492
|
const requestHeartbeatNow = api.runtime?.system?.requestHeartbeatNow;
|
|
@@ -29171,7 +29541,7 @@ function register(api) {
|
|
|
29171
29541
|
await deliverToHermes(pluginConfig, eventText, logger);
|
|
29172
29542
|
task.artifacts.push({
|
|
29173
29543
|
kind: "artifact",
|
|
29174
|
-
artifactId:
|
|
29544
|
+
artifactId: randomUUID7(),
|
|
29175
29545
|
parts: [{ kind: "text", text: `debug: delivered task ${task.id} to Hermes API Server` }]
|
|
29176
29546
|
});
|
|
29177
29547
|
const finalText2 = await generateRuntimeReply(pluginConfig, text, marker);
|
|
@@ -29181,14 +29551,14 @@ function register(api) {
|
|
|
29181
29551
|
if (runtimeKind === "none") {
|
|
29182
29552
|
task.artifacts.push({
|
|
29183
29553
|
kind: "artifact",
|
|
29184
|
-
artifactId:
|
|
29554
|
+
artifactId: randomUUID7(),
|
|
29185
29555
|
parts: [{ kind: "text", text: `debug: runtime delivery disabled for ${marker}` }]
|
|
29186
29556
|
});
|
|
29187
29557
|
return;
|
|
29188
29558
|
}
|
|
29189
29559
|
task.artifacts.push({
|
|
29190
29560
|
kind: "artifact",
|
|
29191
|
-
artifactId:
|
|
29561
|
+
artifactId: randomUUID7(),
|
|
29192
29562
|
parts: [{ kind: "text", text: `debug: enqueued system event for ${marker}` }]
|
|
29193
29563
|
});
|
|
29194
29564
|
if (enqueueSystemEvent) {
|
|
@@ -29272,7 +29642,7 @@ function register(api) {
|
|
|
29272
29642
|
if (!finalText) continue;
|
|
29273
29643
|
task.artifacts.push({
|
|
29274
29644
|
kind: "artifact",
|
|
29275
|
-
artifactId:
|
|
29645
|
+
artifactId: randomUUID7(),
|
|
29276
29646
|
parts: [{ kind: "text", text: finalText }]
|
|
29277
29647
|
});
|
|
29278
29648
|
task.status = {
|
|
@@ -29286,179 +29656,145 @@ function register(api) {
|
|
|
29286
29656
|
logger.info(`[perkos-a2a] Task ${task.id} completed from transcript update`);
|
|
29287
29657
|
}
|
|
29288
29658
|
});
|
|
29289
|
-
api.registerService({
|
|
29290
|
-
id: "perkos-a2a",
|
|
29291
|
-
start: async () => {
|
|
29292
|
-
const registry = lifecycleRegistry();
|
|
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 });
|
|
29299
|
-
await server.start();
|
|
29300
|
-
logger.info(`[perkos-a2a] A2A server started for ${pluginConfig.agentName}`);
|
|
29301
|
-
},
|
|
29302
|
-
stop: async () => {
|
|
29303
|
-
logger.info("[perkos-a2a] A2A server stopping");
|
|
29304
|
-
await server.stop({ reason: "openclaw-service-stop", timeoutMs: 5e3, drainMs: 3e3 });
|
|
29305
|
-
const registry = lifecycleRegistry();
|
|
29306
|
-
if (registry.get(lifecycleKey)?.instanceId === lifecycleInstanceId) registry.delete(lifecycleKey);
|
|
29307
|
-
}
|
|
29308
|
-
});
|
|
29309
29659
|
const chatApiKey = pluginConfig.chat?.apiKey || pluginConfig.relay?.apiKey;
|
|
29310
|
-
const
|
|
29311
|
-
|
|
29312
|
-
|
|
29313
|
-
|
|
29314
|
-
|
|
29315
|
-
|
|
29316
|
-
|
|
29317
|
-
|
|
29318
|
-
|
|
29319
|
-
|
|
29320
|
-
|
|
29321
|
-
|
|
29322
|
-
|
|
29323
|
-
|
|
29324
|
-
|
|
29325
|
-
|
|
29326
|
-
|
|
29327
|
-
|
|
29328
|
-
|
|
29660
|
+
const chatEnabled = Boolean(pluginConfig.chat?.enabled && chatApiKey);
|
|
29661
|
+
const chatStore = chatEnabled ? new ChatStore({ storeRoot: pluginConfig.chat?.storeRoot }) : null;
|
|
29662
|
+
let supervisorClient;
|
|
29663
|
+
const handleSupervisorChatDeliver = async (frame) => {
|
|
29664
|
+
const walletAddress = parseWalletFromIdentity(frame.from);
|
|
29665
|
+
const marker = `[PERKOS_CHAT:${frame.convId}]`;
|
|
29666
|
+
const recentPage = await chatStore?.readPage(frame.convId, {
|
|
29667
|
+
limit: CHAT_CONTEXT_MESSAGE_LIMIT + 1
|
|
29668
|
+
});
|
|
29669
|
+
const recentContext = formatRecentChatContext(recentPage?.messages ?? [], frame.id);
|
|
29670
|
+
const eventText = [
|
|
29671
|
+
marker,
|
|
29672
|
+
`From: ${frame.from}`,
|
|
29673
|
+
`Conversation: ${frame.convId}`,
|
|
29674
|
+
frame.projectId ? `Canonical projectId: ${frame.projectId}` : "",
|
|
29675
|
+
walletAddress ? `Wallet: ${walletAddress}` : "",
|
|
29676
|
+
"",
|
|
29677
|
+
"A user is messaging you in a PerkOS chat conversation.",
|
|
29678
|
+
"To respond, call the `perkos_chat_reply` tool with:",
|
|
29679
|
+
` convId: "${frame.convId}"`,
|
|
29680
|
+
walletAddress ? ` walletAddress: "${walletAddress}"` : ` walletAddress: <derived from From>`,
|
|
29681
|
+
` text: <your reply>`,
|
|
29682
|
+
"",
|
|
29683
|
+
"Recent conversation context (oldest to newest):",
|
|
29684
|
+
recentContext,
|
|
29685
|
+
"",
|
|
29686
|
+
"Use the recent context. Do not ask again for information the user already provided.",
|
|
29687
|
+
"",
|
|
29688
|
+
"Message body:",
|
|
29689
|
+
frame.text
|
|
29690
|
+
].filter(Boolean).join("\n");
|
|
29691
|
+
const directRunPrompt = [
|
|
29692
|
+
marker,
|
|
29693
|
+
`From: ${frame.from}`,
|
|
29694
|
+
`Conversation: ${frame.convId}`,
|
|
29695
|
+
frame.projectId ? `Canonical projectId: ${frame.projectId}` : "",
|
|
29696
|
+
"",
|
|
29697
|
+
"Reply to this PerkOS chat message using your current runtime configuration.",
|
|
29698
|
+
"Return only the reply text. Do not call messaging tools; the PerkOS plugin delivers the returned text.",
|
|
29699
|
+
"Use the recent context and do not ask again for information already provided.",
|
|
29700
|
+
"",
|
|
29701
|
+
"Recent conversation context (oldest to newest):",
|
|
29702
|
+
recentContext,
|
|
29703
|
+
"",
|
|
29704
|
+
"Message body:",
|
|
29705
|
+
frame.text
|
|
29706
|
+
].filter(Boolean).join("\n");
|
|
29707
|
+
const runtimeKind = pluginConfig.runtime?.kind || "openclaw";
|
|
29708
|
+
if (runtimeKind === "hermes" || runtimeKind === "hermes-api") {
|
|
29709
|
+
try {
|
|
29710
|
+
await deliverToHermes(pluginConfig, eventText, logger);
|
|
29711
|
+
} catch (err) {
|
|
29712
|
+
logger.error(`[perkos-chat] hermes delivery failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
29713
|
+
}
|
|
29714
|
+
return;
|
|
29715
|
+
}
|
|
29716
|
+
if (runtimeKind === "none") {
|
|
29717
|
+
logger.info(`[perkos-chat] dropped chat_deliver for ${frame.convId} (runtime=none)`);
|
|
29718
|
+
return;
|
|
29719
|
+
}
|
|
29720
|
+
const chatSessionKey = openclawChatSessionKey(pluginConfig, frame.convId);
|
|
29721
|
+
await prepareOpenClawChatSession(api, pluginConfig, chatSessionKey, logger);
|
|
29722
|
+
if (walletAddress) {
|
|
29723
|
+
const reply = await runOpenClawChatTurn(api, pluginConfig, chatSessionKey, directRunPrompt, frame.text, logger);
|
|
29724
|
+
if (reply) {
|
|
29725
|
+
const result = await supervisorClient.sendChatReply({
|
|
29726
|
+
convId: frame.convId,
|
|
29727
|
+
walletAddress,
|
|
29728
|
+
text: reply,
|
|
29729
|
+
replyTo: frame.id
|
|
29329
29730
|
});
|
|
29330
|
-
|
|
29331
|
-
|
|
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)`);
|
|
29731
|
+
logger.info(`[perkos-chat] embedded reply ${result.delivered ? "delivered" : "queued (offline)"} for ${frame.convId}`);
|
|
29732
|
+
return;
|
|
29420
29733
|
}
|
|
29421
29734
|
}
|
|
29422
|
-
|
|
29735
|
+
if (enqueueSystemEvent) enqueueSystemEvent(eventText, { sessionKey: chatSessionKey });
|
|
29736
|
+
wakeGatewayAgent(requestHeartbeatNow, `[PerkOS-Chat] inbound message in ${frame.convId}`, logger, chatSessionKey);
|
|
29737
|
+
};
|
|
29423
29738
|
if (pluginConfig.chat?.enabled && !chatApiKey) {
|
|
29424
29739
|
logger.error("[perkos-chat] chat.enabled=true but no apiKey (set chat.apiKey or fall back from relay.apiKey)");
|
|
29425
29740
|
}
|
|
29426
|
-
|
|
29427
|
-
|
|
29428
|
-
|
|
29429
|
-
|
|
29430
|
-
|
|
29431
|
-
|
|
29432
|
-
|
|
29433
|
-
|
|
29434
|
-
|
|
29435
|
-
|
|
29741
|
+
const platformAgentId = pluginConfig.platform?.agentId || pluginConfig.platform?.heartbeatUrl?.match(/\/agents\/([^/]+)\/heartbeat/u)?.[1] || pluginConfig.agentName.trim().toLowerCase();
|
|
29742
|
+
supervisorClient = new OpenClawSupervisorClient({
|
|
29743
|
+
ownerHash: supervisorOwnerHash(lifecycleKey),
|
|
29744
|
+
generation: Date.now(),
|
|
29745
|
+
instanceId: lifecycleInstanceId,
|
|
29746
|
+
config: {
|
|
29747
|
+
agentName: pluginConfig.agentName,
|
|
29748
|
+
agentId: platformAgentId,
|
|
29749
|
+
version: pluginVersion,
|
|
29750
|
+
relay: pluginConfig.relay,
|
|
29751
|
+
chat: chatEnabled ? {
|
|
29752
|
+
enabled: true,
|
|
29753
|
+
url: pluginConfig.chat?.url || "wss://chat.perkos.xyz/chat",
|
|
29754
|
+
apiKey: chatApiKey || "",
|
|
29755
|
+
storeRoot: pluginConfig.chat?.storeRoot,
|
|
29756
|
+
defaultHistoryLimit: pluginConfig.chat?.defaultHistoryLimit,
|
|
29757
|
+
minReconnectMs: pluginConfig.chat?.minReconnectMs,
|
|
29758
|
+
maxReconnectMs: pluginConfig.chat?.maxReconnectMs,
|
|
29759
|
+
heartbeatIntervalMs: pluginConfig.chat?.heartbeatIntervalMs
|
|
29760
|
+
} : void 0,
|
|
29761
|
+
platform: {
|
|
29762
|
+
heartbeatUrl: pluginConfig.platform?.heartbeatUrl,
|
|
29763
|
+
heartbeatIntervalMs: pluginConfig.platform?.heartbeatIntervalMs
|
|
29764
|
+
},
|
|
29765
|
+
agentCard: server.getAgentCard()
|
|
29766
|
+
},
|
|
29767
|
+
logger,
|
|
29768
|
+
handlers: {
|
|
29769
|
+
onRelayTask: (message) => server.handleTransportTask(message),
|
|
29770
|
+
onChatDeliver: handleSupervisorChatDeliver,
|
|
29771
|
+
onChannelJoin: async (frame) => {
|
|
29772
|
+
logger.info(`[perkos-chat] joined conv ${frame.convId} (host: ${frame.historyHost}, ${frame.participants.length} participants)`);
|
|
29436
29773
|
}
|
|
29437
|
-
}
|
|
29438
|
-
}
|
|
29439
|
-
|
|
29440
|
-
|
|
29441
|
-
|
|
29442
|
-
|
|
29443
|
-
|
|
29444
|
-
|
|
29445
|
-
|
|
29446
|
-
|
|
29447
|
-
|
|
29448
|
-
|
|
29449
|
-
|
|
29450
|
-
|
|
29451
|
-
|
|
29452
|
-
|
|
29453
|
-
intervalMs: pluginConfig.platform?.heartbeatIntervalMs
|
|
29454
|
-
});
|
|
29455
|
-
},
|
|
29456
|
-
stop: () => {
|
|
29457
|
-
platformHeartbeat?.stop();
|
|
29458
|
-
platformHeartbeat = null;
|
|
29774
|
+
}
|
|
29775
|
+
});
|
|
29776
|
+
server.setRelayTransport(supervisorClient);
|
|
29777
|
+
api.registerService({
|
|
29778
|
+
id: "perkos-a2a",
|
|
29779
|
+
start: async () => {
|
|
29780
|
+
try {
|
|
29781
|
+
await supervisorClient.start();
|
|
29782
|
+
await server.start();
|
|
29783
|
+
logger.info(`[perkos-a2a] A2A server started for ${pluginConfig.agentName}`);
|
|
29784
|
+
} catch (error) {
|
|
29785
|
+
await server.stop({ reason: "openclaw-service-start-failed", timeoutMs: 2e3, drainMs: 0 }).catch(() => void 0);
|
|
29786
|
+
await supervisorClient.stop().catch(() => void 0);
|
|
29787
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
29788
|
+
logger.error(`[perkos-a2a] OWNERSHIP_START_FAILED:${message}`);
|
|
29789
|
+
throw error;
|
|
29459
29790
|
}
|
|
29460
|
-
}
|
|
29461
|
-
|
|
29791
|
+
},
|
|
29792
|
+
stop: async () => {
|
|
29793
|
+
logger.info("[perkos-a2a] A2A server stopping");
|
|
29794
|
+
await server.stop({ reason: "openclaw-service-stop", timeoutMs: 8e3, drainMs: 3e3 });
|
|
29795
|
+
await supervisorClient.stop();
|
|
29796
|
+
}
|
|
29797
|
+
});
|
|
29462
29798
|
api.registerTool({
|
|
29463
29799
|
name: "perkos_a2a_send",
|
|
29464
29800
|
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 +29981,13 @@ function register(api) {
|
|
|
29645
29981
|
required: ["convId", "walletAddress", "text"]
|
|
29646
29982
|
},
|
|
29647
29983
|
async execute(_id, params) {
|
|
29648
|
-
if (!
|
|
29984
|
+
if (!chatEnabled) {
|
|
29649
29985
|
return {
|
|
29650
29986
|
content: [{ type: "text", text: "PerkOS-Chat client is not enabled in this agent's config." }]
|
|
29651
29987
|
};
|
|
29652
29988
|
}
|
|
29653
29989
|
try {
|
|
29654
|
-
const result = await
|
|
29990
|
+
const result = await supervisorClient.sendChatReply({
|
|
29655
29991
|
convId: params.convId,
|
|
29656
29992
|
walletAddress: params.walletAddress,
|
|
29657
29993
|
text: params.text,
|
|
@@ -29692,14 +30028,13 @@ function register(api) {
|
|
|
29692
30028
|
required: ["convId"]
|
|
29693
30029
|
},
|
|
29694
30030
|
async execute(_id, params) {
|
|
29695
|
-
if (!
|
|
30031
|
+
if (!chatStore) {
|
|
29696
30032
|
return {
|
|
29697
30033
|
content: [{ type: "text", text: "PerkOS-Chat client is not enabled in this agent's config." }]
|
|
29698
30034
|
};
|
|
29699
30035
|
}
|
|
29700
30036
|
try {
|
|
29701
|
-
const
|
|
29702
|
-
const page = await store.readPage(params.convId, {
|
|
30037
|
+
const page = await chatStore.readPage(params.convId, {
|
|
29703
30038
|
before: params.before ?? null,
|
|
29704
30039
|
limit: params.limit ?? 50
|
|
29705
30040
|
});
|
|
@@ -29728,9 +30063,9 @@ function register(api) {
|
|
|
29728
30063
|
hasSystemEventInjection: !!enqueueSystemEvent,
|
|
29729
30064
|
protocol: "a2a",
|
|
29730
30065
|
version: "0.8.4",
|
|
29731
|
-
chat:
|
|
30066
|
+
chat: chatEnabled ? {
|
|
29732
30067
|
enabled: true,
|
|
29733
|
-
connected:
|
|
30068
|
+
connected: supervisorClient.isChatConnected(),
|
|
29734
30069
|
url: pluginConfig.chat?.url || "wss://chat.perkos.xyz/chat"
|
|
29735
30070
|
} : { enabled: false, connected: false, url: null }
|
|
29736
30071
|
});
|
|
@@ -29748,8 +30083,8 @@ function register(api) {
|
|
|
29748
30083
|
console.log(`Auth required: ${pluginConfig.auth?.requireApiKey || false}`);
|
|
29749
30084
|
console.log(`Peers: ${Object.keys(pluginConfig.peers).join(", ") || "(none)"}`);
|
|
29750
30085
|
console.log(`System event injection: ${!!enqueueSystemEvent}`);
|
|
29751
|
-
if (
|
|
29752
|
-
console.log(`Chat: enabled (connected=${
|
|
30086
|
+
if (chatEnabled) {
|
|
30087
|
+
console.log(`Chat: enabled (connected=${supervisorClient.isChatConnected()}, url=${pluginConfig.chat?.url || "wss://chat.perkos.xyz/chat"})`);
|
|
29753
30088
|
} else {
|
|
29754
30089
|
console.log("Chat: disabled");
|
|
29755
30090
|
}
|
|
@@ -29770,26 +30105,26 @@ function register(api) {
|
|
|
29770
30105
|
console.log("[perkos-a2a] Detecting environment...\n");
|
|
29771
30106
|
const isMac = process.platform === "darwin";
|
|
29772
30107
|
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: ${
|
|
30108
|
+
const net2 = await detectNetworking();
|
|
30109
|
+
console.log(`Public IP: ${net2.publicIp || "unknown"}`);
|
|
30110
|
+
console.log(`Local IPs: ${net2.localIps.join(", ") || "none"}`);
|
|
30111
|
+
console.log(`Behind NAT: ${net2.isBehindNat ? "yes" : "no"}`);
|
|
30112
|
+
console.log(`Tailscale: ${net2.hasTailscale ? "installed" : "not found"}${net2.tailscaleIp ? ` (${net2.tailscaleIp})` : ""}`);
|
|
29778
30113
|
let portAvailable = true;
|
|
29779
30114
|
try {
|
|
29780
30115
|
const netMod = await import("net");
|
|
29781
|
-
await new Promise((
|
|
30116
|
+
await new Promise((resolve4, reject) => {
|
|
29782
30117
|
const srv = netMod.createServer();
|
|
29783
30118
|
srv.once("error", (err) => {
|
|
29784
30119
|
if (err.code === "EADDRINUSE") {
|
|
29785
30120
|
portAvailable = false;
|
|
29786
|
-
|
|
30121
|
+
resolve4();
|
|
29787
30122
|
} else {
|
|
29788
30123
|
reject(err);
|
|
29789
30124
|
}
|
|
29790
30125
|
});
|
|
29791
30126
|
srv.listen(pluginConfig.port, () => {
|
|
29792
|
-
srv.close(() =>
|
|
30127
|
+
srv.close(() => resolve4());
|
|
29793
30128
|
});
|
|
29794
30129
|
});
|
|
29795
30130
|
} catch {
|
|
@@ -29809,14 +30144,14 @@ Relay: ${pluginConfig.relay?.enabled ? "enabled" : "disabled"}`);
|
|
|
29809
30144
|
console.log(` Local loopback URL for this agent: http://127.0.0.1:${pluginConfig.port}/a2a/jsonrpc`);
|
|
29810
30145
|
console.log(" Configure same-host peers as http://127.0.0.1:<peer-port>, not by shared public IP.");
|
|
29811
30146
|
console.log("");
|
|
29812
|
-
if (!
|
|
30147
|
+
if (!net2.isBehindNat) {
|
|
29813
30148
|
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://${
|
|
30149
|
+
console.log(` Public A2A URL candidate: http://${net2.publicIp}:${pluginConfig.port}/a2a/jsonrpc`);
|
|
29815
30150
|
} else if (pluginConfig.relay?.enabled) {
|
|
29816
30151
|
console.log("You are behind NAT but relay is configured. Bidirectional communication via relay.");
|
|
29817
|
-
} else if (
|
|
30152
|
+
} else if (net2.hasTailscale && net2.tailscaleIp) {
|
|
29818
30153
|
console.log("Use your Tailscale IP for peers.");
|
|
29819
|
-
console.log(` Your A2A URL: http://${
|
|
30154
|
+
console.log(` Your A2A URL: http://${net2.tailscaleIp}:${pluginConfig.port}/a2a/jsonrpc`);
|
|
29820
30155
|
} else {
|
|
29821
30156
|
console.log("You are behind NAT. Options:");
|
|
29822
30157
|
console.log(" 1) Configure a relay hub (recommended) - set relay.url and relay.enabled in config");
|