@perkos/perkos-a2a 0.12.30 → 0.12.32

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