@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.
Files changed (42) hide show
  1. package/README.md +27 -9
  2. package/dist/chat-client.d.ts +6 -0
  3. package/dist/chat-client.d.ts.map +1 -1
  4. package/dist/chat-client.js +80 -22
  5. package/dist/chat-client.js.map +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +702 -421
  8. package/dist/index.js.map +4 -4
  9. package/dist/openclaw-supervisor-cli.d.ts +3 -0
  10. package/dist/openclaw-supervisor-cli.d.ts.map +1 -0
  11. package/dist/openclaw-supervisor-cli.js +18 -0
  12. package/dist/openclaw-supervisor-cli.js.map +1 -0
  13. package/dist/openclaw-supervisor-client.d.ts +68 -0
  14. package/dist/openclaw-supervisor-client.d.ts.map +1 -0
  15. package/dist/openclaw-supervisor-client.js +301 -0
  16. package/dist/openclaw-supervisor-client.js.map +1 -0
  17. package/dist/openclaw-supervisor.d.ts +49 -0
  18. package/dist/openclaw-supervisor.d.ts.map +1 -0
  19. package/dist/openclaw-supervisor.js +443 -0
  20. package/dist/openclaw-supervisor.js.map +1 -0
  21. package/dist/platform-heartbeat.d.ts +5 -0
  22. package/dist/platform-heartbeat.d.ts.map +1 -1
  23. package/dist/platform-heartbeat.js +3 -0
  24. package/dist/platform-heartbeat.js.map +1 -1
  25. package/dist/repair.d.ts +3 -1
  26. package/dist/repair.d.ts.map +1 -1
  27. package/dist/repair.js +42 -35
  28. package/dist/repair.js.map +1 -1
  29. package/dist/runtime-evidence.d.ts +1 -0
  30. package/dist/runtime-evidence.d.ts.map +1 -1
  31. package/dist/runtime-evidence.js +3 -0
  32. package/dist/runtime-evidence.js.map +1 -1
  33. package/dist/server.d.ts +17 -1
  34. package/dist/server.d.ts.map +1 -1
  35. package/dist/server.js +41 -6
  36. package/dist/server.js.map +1 -1
  37. package/dist/supervisor-protocol.d.ts +123 -0
  38. package/dist/supervisor-protocol.d.ts.map +1 -0
  39. package/dist/supervisor-protocol.js +57 -0
  40. package/dist/supervisor-protocol.js.map +1 -0
  41. package/openclaw.plugin.json +1 -1
  42. 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 net = __require("net");
1301
- stream2 = new net.Socket({
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(resolve3, reject) {
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
- resolve3(buf);
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 dirname2 = path.dirname;
18432
+ var dirname3 = path.dirname;
18433
18433
  var basename = path.basename;
18434
18434
  var extname = path.extname;
18435
- var join3 = path.join;
18436
- var resolve3 = path.resolve;
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 = resolve3(root, name);
18471
- var dir = dirname2(loc);
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 resolve4(dir, file) {
18481
+ View.prototype.resolve = function resolve5(dir, file) {
18482
18482
  var ext = this.ext;
18483
- var path2 = join3(dir, file);
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 = join3(dir, basename(file, ext), "index" + ext);
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 join3 = path.join;
19121
+ var join4 = path.join;
19122
19122
  var normalize = path.normalize;
19123
- var resolve3 = path.resolve;
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 ? resolve3(opts.root) : null;
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 = resolve3(String(path2));
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(join3(root, path2));
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 = resolve3(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 = join3(path2, self._index[i]);
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 resolve3 = __require("path").resolve;
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", resolve3("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 resolve3 = path.resolve;
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 ? resolve3(path2) : path2;
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 resolve3 = __require("path").resolve;
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 = resolve3(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 net = __require("net");
24818
+ var net2 = __require("net");
24819
24819
  var tls = __require("tls");
24820
- var { randomBytes, createHash: createHash2 } = __require("crypto");
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 = createHash2("sha1").update(key + GUID).digest("base64");
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 net.connect(options);
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 = net.isIP(options.host) ? "" : options.host;
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: createHash2 } = __require("crypto");
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 = createHash2("sha1").update(key + GUID).digest("base64");
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((resolve3) => {
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
- resolve3({ closeTimedOut });
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((resolve3, reject) => {
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
- resolve3(response);
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((resolve3) => {
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
- resolve3();
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((resolve3) => setTimeout(resolve3, pollMs));
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((resolve3) => setTimeout(resolve3, pollMs));
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
- startRelayClient() {
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 net = await detectNetworking();
27560
+ const net2 = await detectNetworking();
27528
27561
  if (!this.isGenerationActive(generation, "starting")) return;
27529
- if (net.isBehindNat && !net.hasTailscale) {
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 (net.isBehindNat && net.hasTailscale && net.tailscaleIp) {
27577
+ if (net2.isBehindNat && net2.hasTailscale && net2.tailscaleIp) {
27545
27578
  this.logger.info(
27546
- `[perkos-a2a] Behind NAT but Tailscale detected (${net.tailscaleIp}). Starting server.`
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 randomUUID6 } from "crypto";
27682
- import { readFileSync as readFileSync3 } from "node:fs";
27683
- import { dirname, resolve as resolve2 } from "node:path";
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((resolve3, reject) => {
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
- resolve3({
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.connect();
28784
+ const generation = ++this.generation;
28785
+ this.connect(generation);
28363
28786
  }
28364
28787
  stop() {
28365
- this.stopped = true;
28366
- this.clearTimers();
28367
- if (this.ws) {
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
- this.ws.close(1e3, "client shutting down");
28816
+ socket.close(1e3, "client shutting down");
28370
28817
  } catch {
28818
+ finish(false);
28371
28819
  }
28372
- this.ws = null;
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 = randomUUID5();
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.stopped) return;
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
- this.ws?.close(4002, "auth timeout");
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.stopped) return;
29107
+ scheduleReconnect(generation) {
29108
+ if (!this.isCurrent(generation)) return;
28656
29109
  const delay = this.reconnectMs;
28657
- this.reconnectTimer = setTimeout(() => this.connect(), delay);
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(readFileSync3(new URL("../openclaw.plugin.json", import.meta.url), "utf8"));
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: randomUUID6(), updatedAt: Date.now() },
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: randomUUID6(), updatedAt: Date.now() },
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 : randomUUID6();
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: randomUUID6(),
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: resolve2(dirname(fileURLToPath(import.meta.url)), ".."),
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 = randomUUID6();
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: randomUUID6(),
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: randomUUID6(),
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: randomUUID6(),
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: randomUUID6(),
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
- 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 });
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: 5e3, drainMs: 3e3 });
29305
- const registry = lifecycleRegistry();
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 (!chatClient) {
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 chatClient.sendReply({
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 (!chatClient) {
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 store = chatClient.getStore();
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: chatClient ? {
30012
+ chat: chatEnabled ? {
29732
30013
  enabled: true,
29733
- connected: chatClient.isConnected(),
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 (chatClient) {
29752
- console.log(`Chat: enabled (connected=${chatClient.isConnected()}, url=${pluginConfig.chat?.url || "wss://chat.perkos.xyz/chat"})`);
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 net = await detectNetworking();
29774
- console.log(`Public IP: ${net.publicIp || "unknown"}`);
29775
- console.log(`Local IPs: ${net.localIps.join(", ") || "none"}`);
29776
- console.log(`Behind NAT: ${net.isBehindNat ? "yes" : "no"}`);
29777
- console.log(`Tailscale: ${net.hasTailscale ? "installed" : "not found"}${net.tailscaleIp ? ` (${net.tailscaleIp})` : ""}`);
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((resolve3, reject) => {
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
- resolve3();
30067
+ resolve4();
29787
30068
  } else {
29788
30069
  reject(err);
29789
30070
  }
29790
30071
  });
29791
30072
  srv.listen(pluginConfig.port, () => {
29792
- srv.close(() => resolve3());
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 (!net.isBehindNat) {
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://${net.publicIp}:${pluginConfig.port}/a2a/jsonrpc`);
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 (net.hasTailscale && net.tailscaleIp) {
30098
+ } else if (net2.hasTailscale && net2.tailscaleIp) {
29818
30099
  console.log("Use your Tailscale IP for peers.");
29819
- console.log(` Your A2A URL: http://${net.tailscaleIp}:${pluginConfig.port}/a2a/jsonrpc`);
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");