@perkos/perkos-a2a 0.12.31 → 0.12.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +41 -9
  2. package/dist/chat-client.d.ts +6 -0
  3. package/dist/chat-client.d.ts.map +1 -1
  4. package/dist/chat-client.js +80 -22
  5. package/dist/chat-client.js.map +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +759 -424
  8. package/dist/index.js.map +4 -4
  9. package/dist/openclaw-supervisor-cli.d.ts +3 -0
  10. package/dist/openclaw-supervisor-cli.d.ts.map +1 -0
  11. package/dist/openclaw-supervisor-cli.js +43 -0
  12. package/dist/openclaw-supervisor-cli.js.map +1 -0
  13. package/dist/openclaw-supervisor-client.d.ts +71 -0
  14. package/dist/openclaw-supervisor-client.d.ts.map +1 -0
  15. package/dist/openclaw-supervisor-client.js +343 -0
  16. package/dist/openclaw-supervisor-client.js.map +1 -0
  17. package/dist/openclaw-supervisor.d.ts +49 -0
  18. package/dist/openclaw-supervisor.d.ts.map +1 -0
  19. package/dist/openclaw-supervisor.js +447 -0
  20. package/dist/openclaw-supervisor.js.map +1 -0
  21. package/dist/platform-heartbeat.d.ts +5 -0
  22. package/dist/platform-heartbeat.d.ts.map +1 -1
  23. package/dist/platform-heartbeat.js +3 -0
  24. package/dist/platform-heartbeat.js.map +1 -1
  25. package/dist/repair.d.ts +14 -2
  26. package/dist/repair.d.ts.map +1 -1
  27. package/dist/repair.js +126 -44
  28. package/dist/repair.js.map +1 -1
  29. package/dist/runtime-evidence.d.ts +6 -3
  30. package/dist/runtime-evidence.d.ts.map +1 -1
  31. package/dist/runtime-evidence.js +7 -1
  32. package/dist/runtime-evidence.js.map +1 -1
  33. package/dist/server.d.ts +20 -1
  34. package/dist/server.d.ts.map +1 -1
  35. package/dist/server.js +47 -7
  36. package/dist/server.js.map +1 -1
  37. package/dist/supervisor-protocol.d.ts +138 -0
  38. package/dist/supervisor-protocol.d.ts.map +1 -0
  39. package/dist/supervisor-protocol.js +60 -0
  40. package/dist/supervisor-protocol.js.map +1 -0
  41. package/openclaw.plugin.json +1 -1
  42. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -1297,8 +1297,8 @@ var require_node = __commonJS({
1297
1297
  break;
1298
1298
  case "PIPE":
1299
1299
  case "TCP":
1300
- var net = __require("net");
1301
- stream2 = new net.Socket({
1300
+ var net2 = __require("net");
1301
+ stream2 = new net2.Socket({
1302
1302
  fd: fd2,
1303
1303
  readable: false,
1304
1304
  writable: true
@@ -4944,10 +4944,10 @@ var require_raw_body = __commonJS({
4944
4944
  if (done) {
4945
4945
  return readStream(stream, encoding, length, limit, wrap(done));
4946
4946
  }
4947
- return new Promise(function executor(resolve3, reject) {
4947
+ return new Promise(function executor(resolve4, reject) {
4948
4948
  readStream(stream, encoding, length, limit, function onRead(err, buf) {
4949
4949
  if (err) return reject(err);
4950
- resolve3(buf);
4950
+ resolve4(buf);
4951
4951
  });
4952
4952
  });
4953
4953
  }
@@ -18429,11 +18429,11 @@ var require_view = __commonJS({
18429
18429
  var debug = require_src()("express:view");
18430
18430
  var path = __require("path");
18431
18431
  var fs = __require("fs");
18432
- var dirname2 = path.dirname;
18432
+ var dirname3 = path.dirname;
18433
18433
  var basename = path.basename;
18434
18434
  var extname = path.extname;
18435
- var join3 = path.join;
18436
- var resolve3 = path.resolve;
18435
+ var join4 = path.join;
18436
+ var resolve4 = path.resolve;
18437
18437
  module.exports = View;
18438
18438
  function View(name, options) {
18439
18439
  var opts = options || {};
@@ -18467,8 +18467,8 @@ var require_view = __commonJS({
18467
18467
  debug('lookup "%s"', name);
18468
18468
  for (var i = 0; i < roots.length && !path2; i++) {
18469
18469
  var root = roots[i];
18470
- var loc = resolve3(root, name);
18471
- var dir = dirname2(loc);
18470
+ var loc = resolve4(root, name);
18471
+ var dir = dirname3(loc);
18472
18472
  var file = basename(loc);
18473
18473
  path2 = this.resolve(dir, file);
18474
18474
  }
@@ -18478,14 +18478,14 @@ var require_view = __commonJS({
18478
18478
  debug('render "%s"', this.path);
18479
18479
  this.engine(this.path, options, callback);
18480
18480
  };
18481
- View.prototype.resolve = function resolve4(dir, file) {
18481
+ View.prototype.resolve = function resolve5(dir, file) {
18482
18482
  var ext = this.ext;
18483
- var path2 = join3(dir, file);
18483
+ var path2 = join4(dir, file);
18484
18484
  var stat = tryStat(path2);
18485
18485
  if (stat && stat.isFile()) {
18486
18486
  return path2;
18487
18487
  }
18488
- path2 = join3(dir, basename(file, ext), "index" + ext);
18488
+ path2 = join4(dir, basename(file, ext), "index" + ext);
18489
18489
  stat = tryStat(path2);
18490
18490
  if (stat && stat.isFile()) {
18491
18491
  return path2;
@@ -19118,9 +19118,9 @@ var require_send = __commonJS({
19118
19118
  var Stream = __require("stream");
19119
19119
  var util = __require("util");
19120
19120
  var extname = path.extname;
19121
- var join3 = path.join;
19121
+ var join4 = path.join;
19122
19122
  var normalize = path.normalize;
19123
- var resolve3 = path.resolve;
19123
+ var resolve4 = path.resolve;
19124
19124
  var sep = path.sep;
19125
19125
  var BYTES_RANGE_REGEXP = /^ *bytes=/;
19126
19126
  var MAX_MAXAGE = 60 * 60 * 24 * 365 * 1e3;
@@ -19157,7 +19157,7 @@ var require_send = __commonJS({
19157
19157
  this._maxage = opts.maxAge || opts.maxage;
19158
19158
  this._maxage = typeof this._maxage === "string" ? ms(this._maxage) : Number(this._maxage);
19159
19159
  this._maxage = !isNaN(this._maxage) ? Math.min(Math.max(0, this._maxage), MAX_MAXAGE) : 0;
19160
- this._root = opts.root ? resolve3(opts.root) : null;
19160
+ this._root = opts.root ? resolve4(opts.root) : null;
19161
19161
  if (!this._root && opts.from) {
19162
19162
  this.from(opts.from);
19163
19163
  }
@@ -19181,7 +19181,7 @@ var require_send = __commonJS({
19181
19181
  return this;
19182
19182
  }, "send.index: pass index as option");
19183
19183
  SendStream.prototype.root = function root(path2) {
19184
- this._root = resolve3(String(path2));
19184
+ this._root = resolve4(String(path2));
19185
19185
  debug("root %s", this._root);
19186
19186
  return this;
19187
19187
  };
@@ -19337,7 +19337,7 @@ var require_send = __commonJS({
19337
19337
  return res;
19338
19338
  }
19339
19339
  parts = path2.split(sep);
19340
- path2 = normalize(join3(root, path2));
19340
+ path2 = normalize(join4(root, path2));
19341
19341
  } else {
19342
19342
  if (UP_PATH_REGEXP.test(path2)) {
19343
19343
  debug('malicious path "%s"', path2);
@@ -19345,7 +19345,7 @@ var require_send = __commonJS({
19345
19345
  return res;
19346
19346
  }
19347
19347
  parts = normalize(path2).split(sep);
19348
- path2 = resolve3(path2);
19348
+ path2 = resolve4(path2);
19349
19349
  }
19350
19350
  if (containsDotFile(parts)) {
19351
19351
  var access = this._dotfiles;
@@ -19472,7 +19472,7 @@ var require_send = __commonJS({
19472
19472
  if (err) return self.onStatError(err);
19473
19473
  return self.error(404);
19474
19474
  }
19475
- var p = join3(path2, self._index[i]);
19475
+ var p = join4(path2, self._index[i]);
19476
19476
  debug('stat "%s"', p);
19477
19477
  fs.stat(p, function(err2, stat) {
19478
19478
  if (err2) return next(err2);
@@ -20625,7 +20625,7 @@ var require_application = __commonJS({
20625
20625
  var deprecate = require_depd()("express");
20626
20626
  var flatten = require_array_flatten();
20627
20627
  var merge = require_utils_merge();
20628
- var resolve3 = __require("path").resolve;
20628
+ var resolve4 = __require("path").resolve;
20629
20629
  var setPrototypeOf = require_setprototypeof();
20630
20630
  var hasOwnProperty = Object.prototype.hasOwnProperty;
20631
20631
  var slice = Array.prototype.slice;
@@ -20664,7 +20664,7 @@ var require_application = __commonJS({
20664
20664
  this.mountpath = "/";
20665
20665
  this.locals.settings = this.settings;
20666
20666
  this.set("view", View);
20667
- this.set("views", resolve3("views"));
20667
+ this.set("views", resolve4("views"));
20668
20668
  this.set("jsonp callback name", "callback");
20669
20669
  if (env === "production") {
20670
20670
  this.enable("view cache");
@@ -21909,7 +21909,7 @@ var require_response = __commonJS({
21909
21909
  var send = require_send();
21910
21910
  var extname = path.extname;
21911
21911
  var mime = send.mime;
21912
- var resolve3 = path.resolve;
21912
+ var resolve4 = path.resolve;
21913
21913
  var vary = require_vary();
21914
21914
  var res = Object.create(http.ServerResponse.prototype);
21915
21915
  module.exports = res;
@@ -22168,7 +22168,7 @@ var require_response = __commonJS({
22168
22168
  }
22169
22169
  opts = Object.create(opts);
22170
22170
  opts.headers = headers;
22171
- var fullPath = !opts.root ? resolve3(path2) : path2;
22171
+ var fullPath = !opts.root ? resolve4(path2) : path2;
22172
22172
  return this.sendFile(fullPath, opts, done);
22173
22173
  };
22174
22174
  res.contentType = res.type = function contentType(type) {
@@ -22434,7 +22434,7 @@ var require_serve_static = __commonJS({
22434
22434
  var encodeUrl = require_encodeurl();
22435
22435
  var escapeHtml = require_escape_html();
22436
22436
  var parseUrl = require_parseurl();
22437
- var resolve3 = __require("path").resolve;
22437
+ var resolve4 = __require("path").resolve;
22438
22438
  var send = require_send();
22439
22439
  var url = __require("url");
22440
22440
  module.exports = serveStatic;
@@ -22454,7 +22454,7 @@ var require_serve_static = __commonJS({
22454
22454
  throw new TypeError("option setHeaders must be function");
22455
22455
  }
22456
22456
  opts.maxage = opts.maxage || opts.maxAge || 0;
22457
- opts.root = resolve3(root);
22457
+ opts.root = resolve4(root);
22458
22458
  var onDirectory = redirect ? createRedirectDirectoryListener() : createNotFoundDirectoryListener();
22459
22459
  return function serveStatic2(req, res, next) {
22460
22460
  if (req.method !== "GET" && req.method !== "HEAD") {
@@ -24815,9 +24815,9 @@ var require_websocket = __commonJS({
24815
24815
  var EventEmitter = __require("events");
24816
24816
  var https = __require("https");
24817
24817
  var http = __require("http");
24818
- var net = __require("net");
24818
+ var net2 = __require("net");
24819
24819
  var tls = __require("tls");
24820
- var { randomBytes, createHash: createHash2 } = __require("crypto");
24820
+ var { randomBytes, createHash: createHash3 } = __require("crypto");
24821
24821
  var { Duplex, Readable } = __require("stream");
24822
24822
  var { URL: URL2 } = __require("url");
24823
24823
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -25477,7 +25477,7 @@ var require_websocket = __commonJS({
25477
25477
  abortHandshake(websocket, socket, "Invalid Upgrade header");
25478
25478
  return;
25479
25479
  }
25480
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
25480
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
25481
25481
  if (res.headers["sec-websocket-accept"] !== digest) {
25482
25482
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
25483
25483
  return;
@@ -25549,12 +25549,12 @@ var require_websocket = __commonJS({
25549
25549
  }
25550
25550
  function netConnect(options) {
25551
25551
  options.path = options.socketPath;
25552
- return net.connect(options);
25552
+ return net2.connect(options);
25553
25553
  }
25554
25554
  function tlsConnect(options) {
25555
25555
  options.path = void 0;
25556
25556
  if (!options.servername && options.servername !== "") {
25557
- options.servername = net.isIP(options.host) ? "" : options.host;
25557
+ options.servername = net2.isIP(options.host) ? "" : options.host;
25558
25558
  }
25559
25559
  return tls.connect(options);
25560
25560
  }
@@ -25844,7 +25844,7 @@ var require_websocket_server = __commonJS({
25844
25844
  var EventEmitter = __require("events");
25845
25845
  var http = __require("http");
25846
25846
  var { Duplex } = __require("stream");
25847
- var { createHash: createHash2 } = __require("crypto");
25847
+ var { createHash: createHash3 } = __require("crypto");
25848
25848
  var extension2 = require_extension();
25849
25849
  var PerMessageDeflate2 = require_permessage_deflate();
25850
25850
  var subprotocol2 = require_subprotocol();
@@ -26145,7 +26145,7 @@ var require_websocket_server = __commonJS({
26145
26145
  );
26146
26146
  }
26147
26147
  if (this._state > RUNNING) return abortHandshake(socket, 503);
26148
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
26148
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
26149
26149
  const headers = [
26150
26150
  "HTTP/1.1 101 Switching Protocols",
26151
26151
  "Upgrade: websocket",
@@ -26291,13 +26291,13 @@ var RelayClient = class {
26291
26291
  async stopAndWait(timeoutMs = 5e3) {
26292
26292
  const socket = this.beginStop();
26293
26293
  if (!socket || socket.readyState === wrapper_default.CLOSED) return { closeTimedOut: false };
26294
- return new Promise((resolve3) => {
26294
+ return new Promise((resolve4) => {
26295
26295
  let settled = false;
26296
26296
  const finish = (closeTimedOut) => {
26297
26297
  if (settled) return;
26298
26298
  settled = true;
26299
26299
  clearTimeout(timer);
26300
- resolve3({ closeTimedOut });
26300
+ resolve4({ closeTimedOut });
26301
26301
  };
26302
26302
  const timer = setTimeout(() => {
26303
26303
  finish(true);
@@ -26488,7 +26488,7 @@ var RelayClient = class {
26488
26488
  }
26489
26489
  }
26490
26490
  async sendAndWait(msg, expectedType, timeoutMs) {
26491
- return new Promise((resolve3, reject) => {
26491
+ return new Promise((resolve4, reject) => {
26492
26492
  const timer = setTimeout(() => {
26493
26493
  this.pendingCallbacks.delete(msg.id);
26494
26494
  reject(new Error(`Relay request timed out after ${timeoutMs}ms`));
@@ -26500,7 +26500,7 @@ var RelayClient = class {
26500
26500
  if (response.type === "error") {
26501
26501
  reject(new Error(`Relay error: ${response.payload.message}`));
26502
26502
  } else {
26503
- resolve3(response);
26503
+ resolve4(response);
26504
26504
  }
26505
26505
  },
26506
26506
  cancel: (error) => {
@@ -26603,13 +26603,13 @@ var RelayHub = class {
26603
26603
  for (const agent of this.agents.values()) {
26604
26604
  agent.ws.close(1001, "Relay shutting down");
26605
26605
  }
26606
- await new Promise((resolve3) => {
26606
+ await new Promise((resolve4) => {
26607
26607
  let settled = false;
26608
26608
  const done = () => {
26609
26609
  if (settled) return;
26610
26610
  settled = true;
26611
26611
  clearTimeout(timer);
26612
- resolve3();
26612
+ resolve4();
26613
26613
  };
26614
26614
  const timer = setTimeout(() => {
26615
26615
  for (const client of wss.clients) client.terminate();
@@ -27008,6 +27008,8 @@ var A2AServer = class {
27008
27008
  logger;
27009
27009
  clientOnly = false;
27010
27010
  relayClient = null;
27011
+ externalRelayTransport = false;
27012
+ relayOwnership;
27011
27013
  relayHub = null;
27012
27014
  messageInjector = null;
27013
27015
  taskResultHandler = null;
@@ -27019,9 +27021,10 @@ var A2AServer = class {
27019
27021
  startPromise = null;
27020
27022
  stopPromise = null;
27021
27023
  operations = /* @__PURE__ */ new Set();
27022
- constructor(config, logger) {
27024
+ constructor(config, logger, options = {}) {
27023
27025
  this.config = config;
27024
27026
  this.logger = logger || { info: console.log, error: console.error };
27027
+ this.relayOwnership = options.relayOwnership ?? "local";
27025
27028
  this.app = (0, import_express.default)();
27026
27029
  this.app.use(import_express.default.json());
27027
27030
  this.agentCard = {
@@ -27047,6 +27050,14 @@ var A2AServer = class {
27047
27050
  setTaskFailureHandler(handler) {
27048
27051
  this.taskFailureHandler = handler;
27049
27052
  }
27053
+ /** Use a process-external owner for relay transport (OpenClaw supervisor). */
27054
+ setRelayTransport(transport) {
27055
+ if (this.lifecycleState !== "new" && this.lifecycleState !== "stopped") {
27056
+ throw new Error("relay transport must be configured before start");
27057
+ }
27058
+ this.relayClient = transport;
27059
+ this.externalRelayTransport = true;
27060
+ }
27050
27061
  getTasks() {
27051
27062
  return this.tasks.values();
27052
27063
  }
@@ -27062,6 +27073,9 @@ var A2AServer = class {
27062
27073
  isRelayConnected() {
27063
27074
  return this.relayClient?.isConnected() || false;
27064
27075
  }
27076
+ getAgentCard() {
27077
+ return structuredClone(this.agentCard);
27078
+ }
27065
27079
  authMiddleware() {
27066
27080
  return (req, res, next) => {
27067
27081
  if (!this.config.auth?.requireApiKey) {
@@ -27324,7 +27338,7 @@ var A2AServer = class {
27324
27338
  async waitForRemoteTaskTerminal(targetUrl, headers, taskId, timeoutMs = 45e3, pollMs = 1e3) {
27325
27339
  const deadline = Date.now() + timeoutMs;
27326
27340
  while (Date.now() < deadline) {
27327
- await new Promise((resolve3) => setTimeout(resolve3, pollMs));
27341
+ await new Promise((resolve4) => setTimeout(resolve4, pollMs));
27328
27342
  const response = await fetch(`${targetUrl}/a2a/jsonrpc`, {
27329
27343
  method: "POST",
27330
27344
  headers,
@@ -27348,7 +27362,7 @@ var A2AServer = class {
27348
27362
  if (task && isTerminalTaskState(task.status?.state)) {
27349
27363
  return this.success(randomUUID4(), structuredClone(task));
27350
27364
  }
27351
- await new Promise((resolve3) => setTimeout(resolve3, pollMs));
27365
+ await new Promise((resolve4) => setTimeout(resolve4, pollMs));
27352
27366
  }
27353
27367
  return null;
27354
27368
  }
@@ -27454,9 +27468,33 @@ var A2AServer = class {
27454
27468
  });
27455
27469
  this.trackOperation(operation);
27456
27470
  }
27457
- startRelayClient() {
27471
+ /** Process a relay task delivered by the OpenClaw supervisor over IPC. */
27472
+ async handleTransportTask(msg) {
27473
+ if (this.lifecycleState !== "running") {
27474
+ return this.error(String(msg.payload?.id ?? msg.id), 503, "SERVER_RELOADING");
27475
+ }
27476
+ const payload = msg.payload;
27477
+ const params = payload.params || payload;
27478
+ const rpcId = String(payload.id || msg.id);
27479
+ const response = await this.handleSendMessage(params, rpcId);
27480
+ const task = response.result;
27481
+ if (task?.id && !isTerminalTaskState(task.status?.state)) {
27482
+ const terminal = await this.waitForLocalTaskTerminal(task.id);
27483
+ if (terminal) return terminal;
27484
+ }
27485
+ return response;
27486
+ }
27487
+ async startRelayClient() {
27458
27488
  const relay = this.config.relay;
27459
27489
  if (!relay?.enabled || !relay?.url) return;
27490
+ if (this.externalRelayTransport && this.relayClient) {
27491
+ await this.relayClient.start();
27492
+ this.logger.info(`[perkos-a2a] Relay transport delegated to supervisor for ${relay.url}`);
27493
+ return;
27494
+ }
27495
+ if (this.relayOwnership === "external-required") {
27496
+ throw new Error("OWNERSHIP_PRIMITIVE_UNAVAILABLE:OpenClaw relay transport requires the process-external supervisor");
27497
+ }
27460
27498
  this.relayClient = new RelayClient({
27461
27499
  agentName: this.config.agentName,
27462
27500
  relay,
@@ -27509,7 +27547,7 @@ var A2AServer = class {
27509
27547
  if (mode === "client-only") {
27510
27548
  if (!this.isGenerationActive(generation, "starting")) return;
27511
27549
  this.clientOnly = true;
27512
- this.startRelayClient();
27550
+ await this.startRelayClient();
27513
27551
  if (!this.config.relay?.enabled) {
27514
27552
  this.logger.info(
27515
27553
  "[perkos-a2a] Running in client-only mode. Configure relay for NAT traversal or set up Tailscale/tunnel."
@@ -27520,19 +27558,19 @@ var A2AServer = class {
27520
27558
  if (mode === "full") {
27521
27559
  await this.tryListen(this.config.port, 1, generation);
27522
27560
  if (!this.isGenerationActive(generation, "starting")) return;
27523
- this.startRelayClient();
27561
+ await this.startRelayClient();
27524
27562
  return;
27525
27563
  }
27526
27564
  try {
27527
- const net = await detectNetworking();
27565
+ const net2 = await detectNetworking();
27528
27566
  if (!this.isGenerationActive(generation, "starting")) return;
27529
- if (net.isBehindNat && !net.hasTailscale) {
27567
+ if (net2.isBehindNat && !net2.hasTailscale) {
27530
27568
  if (this.config.relay?.enabled) {
27531
27569
  this.logger.info(
27532
27570
  "[perkos-a2a] Behind NAT, using relay for bidirectional communication"
27533
27571
  );
27534
27572
  this.clientOnly = true;
27535
- this.startRelayClient();
27573
+ await this.startRelayClient();
27536
27574
  } else {
27537
27575
  this.logger.info(
27538
27576
  "[perkos-a2a] Running in client-only mode (behind NAT). Configure relay or Tailscale for bidirectional A2A."
@@ -27541,16 +27579,16 @@ var A2AServer = class {
27541
27579
  }
27542
27580
  return;
27543
27581
  }
27544
- if (net.isBehindNat && net.hasTailscale && net.tailscaleIp) {
27582
+ if (net2.isBehindNat && net2.hasTailscale && net2.tailscaleIp) {
27545
27583
  this.logger.info(
27546
- `[perkos-a2a] Behind NAT but Tailscale detected (${net.tailscaleIp}). Starting server.`
27584
+ `[perkos-a2a] Behind NAT but Tailscale detected (${net2.tailscaleIp}). Starting server.`
27547
27585
  );
27548
27586
  }
27549
27587
  } catch {
27550
27588
  }
27551
27589
  await this.tryListen(this.config.port, 1, generation);
27552
27590
  if (!this.isGenerationActive(generation, "starting")) return;
27553
- this.startRelayClient();
27591
+ await this.startRelayClient();
27554
27592
  }
27555
27593
  stop(options = {}) {
27556
27594
  if (this.lifecycleState === "stopping" && this.stopPromise) return this.stopPromise;
@@ -27648,7 +27686,7 @@ var A2AServer = class {
27648
27686
  const deadline = Date.now() + timeoutMs;
27649
27687
  const remaining = () => Math.max(1, deadline - Date.now());
27650
27688
  const relayClient = this.relayClient;
27651
- this.relayClient = null;
27689
+ if (!this.externalRelayTransport) this.relayClient = null;
27652
27690
  await relayClient?.stopAndWait(remaining());
27653
27691
  const relayHub = this.relayHub;
27654
27692
  this.relayHub = null;
@@ -27678,10 +27716,10 @@ var A2AServer = class {
27678
27716
  };
27679
27717
 
27680
27718
  // src/index.ts
27681
- import { randomUUID as randomUUID6 } from "crypto";
27682
- import { readFileSync as readFileSync3 } from "node:fs";
27683
- import { dirname, resolve as resolve2 } from "node:path";
27684
- import { fileURLToPath } from "node:url";
27719
+ import { randomUUID as randomUUID7 } from "crypto";
27720
+ import { readFileSync as readFileSync5 } from "node:fs";
27721
+ import { dirname as dirname2, resolve as resolve3 } from "node:path";
27722
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
27685
27723
 
27686
27724
  // src/agentic-actions.ts
27687
27725
  var FALLBACK_AGENT_ALIASES = [
@@ -28096,9 +28134,6 @@ ${replyResultText2}` : void 0,
28096
28134
  return true;
28097
28135
  }
28098
28136
 
28099
- // src/chat-client.ts
28100
- import { randomUUID as randomUUID5 } from "node:crypto";
28101
-
28102
28137
  // src/chat-store.ts
28103
28138
  import { createHash } from "node:crypto";
28104
28139
  import { createReadStream } from "node:fs";
@@ -28226,7 +28261,7 @@ var ChatStore = class {
28226
28261
  let firstMessageAt = null;
28227
28262
  let lastMessageAt = null;
28228
28263
  let leftover = "";
28229
- return new Promise((resolve3, reject) => {
28264
+ return new Promise((resolve4, reject) => {
28230
28265
  stream.on("data", (chunk) => {
28231
28266
  const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
28232
28267
  hash.update(text);
@@ -28266,7 +28301,7 @@ var ChatStore = class {
28266
28301
  hash.update(metaRaw);
28267
28302
  } catch {
28268
28303
  }
28269
- resolve3({
28304
+ resolve4({
28270
28305
  transcriptHash: hash.digest("hex"),
28271
28306
  hashAlgo: "sha256",
28272
28307
  messageCount,
@@ -28301,7 +28336,439 @@ function isNotFound(err) {
28301
28336
  return !!err && typeof err === "object" && err.code === "ENOENT";
28302
28337
  }
28303
28338
 
28339
+ // src/openclaw-supervisor-client.ts
28340
+ import { randomUUID as randomUUID5 } from "node:crypto";
28341
+ import { spawn } from "node:child_process";
28342
+ import { existsSync, readFileSync as readFileSync3, unlinkSync } from "node:fs";
28343
+ import { homedir as homedir3 } from "node:os";
28344
+ import net from "node:net";
28345
+ import { resolve } from "node:path";
28346
+ import { fileURLToPath } from "node:url";
28347
+
28348
+ // src/supervisor-protocol.ts
28349
+ import { createHash as createHash2 } from "node:crypto";
28350
+ import { chmodSync, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "node:fs";
28351
+ import { tmpdir } from "node:os";
28352
+ import { dirname, join as join2 } from "node:path";
28353
+ var SUPERVISOR_PROTOCOL_VERSION = 2;
28354
+ var SUPERVISOR_RENEW_MS = 5e3;
28355
+ function supervisorOwnerHash(lifecycleKey) {
28356
+ return createHash2("sha256").update(lifecycleKey).digest("hex");
28357
+ }
28358
+ function supervisorDir(stateDir) {
28359
+ return join2(stateDir, "perkos-a2a", "supervisors");
28360
+ }
28361
+ function supervisorSocketPath(stateDir, ownerHash) {
28362
+ const preferred = join2(supervisorDir(stateDir), `${ownerHash.slice(0, 32)}.sock`);
28363
+ if (Buffer.byteLength(preferred) <= 96) return preferred;
28364
+ const uid = typeof process.getuid === "function" ? process.getuid() : "user";
28365
+ return join2(tmpdir(), `perkos-a2a-${uid}`, `${ownerHash.slice(0, 32)}.sock`);
28366
+ }
28367
+ function supervisorBootstrapPath(stateDir, ownerHash) {
28368
+ return join2(supervisorDir(stateDir), `${ownerHash}.bootstrap.json`);
28369
+ }
28370
+ function currentBootId() {
28371
+ try {
28372
+ return readFileSync2("/proc/sys/kernel/random/boot_id", "utf8").trim();
28373
+ } catch {
28374
+ return `process-boot-${Math.floor(Date.now() - process.uptime() * 1e3)}`;
28375
+ }
28376
+ }
28377
+ function processStartIdentity(pid = process.pid) {
28378
+ try {
28379
+ const stat = readFileSync2(`/proc/${pid}/stat`, "utf8");
28380
+ const afterName = stat.slice(stat.lastIndexOf(")") + 2).trim().split(/\s+/u);
28381
+ return `linux-start-ticks:${afterName[19] ?? "unknown"}`;
28382
+ } catch {
28383
+ if (pid === process.pid) return `epoch-ms:${Math.floor(Date.now() - process.uptime() * 1e3)}`;
28384
+ return "unknown";
28385
+ }
28386
+ }
28387
+ function encodeSupervisorMessage(message) {
28388
+ return `${JSON.stringify(message)}
28389
+ `;
28390
+ }
28391
+
28392
+ // src/openclaw-supervisor-client.ts
28393
+ var OpenClawSupervisorClient = class {
28394
+ stateDir;
28395
+ ownerHash;
28396
+ socketPath;
28397
+ config;
28398
+ logger;
28399
+ handlers;
28400
+ supervisorCliPath;
28401
+ lease;
28402
+ socket = null;
28403
+ startPromise = null;
28404
+ stopPromise = null;
28405
+ renewTimer = null;
28406
+ pending = /* @__PURE__ */ new Map();
28407
+ health = null;
28408
+ stopped = true;
28409
+ constructor(input) {
28410
+ this.stateDir = resolve(input.stateDir ?? process.env.OPENCLAW_STATE_DIR ?? `${homedir3()}/.openclaw`);
28411
+ this.ownerHash = input.ownerHash;
28412
+ this.socketPath = supervisorSocketPath(this.stateDir, this.ownerHash);
28413
+ this.config = input.config;
28414
+ this.logger = input.logger;
28415
+ this.handlers = input.handlers;
28416
+ this.supervisorCliPath = input.supervisorCliPath;
28417
+ this.lease = {
28418
+ ownerKey: input.ownerHash,
28419
+ instanceId: input.instanceId,
28420
+ generation: input.generation,
28421
+ parentPid: process.pid,
28422
+ parentStartTime: processStartIdentity(),
28423
+ bootId: currentBootId(),
28424
+ lastRenewedAt: (/* @__PURE__ */ new Date()).toISOString()
28425
+ };
28426
+ }
28427
+ isConnected() {
28428
+ return this.health?.relayConnected === true && this.health?.relayRegistered === true;
28429
+ }
28430
+ isChatConnected() {
28431
+ return this.health?.chatConnected === true;
28432
+ }
28433
+ currentHealth() {
28434
+ return this.health;
28435
+ }
28436
+ start() {
28437
+ if (!this.stopped && this.socket && !this.socket.destroyed) return Promise.resolve();
28438
+ if (this.startPromise) return this.startPromise;
28439
+ this.stopped = false;
28440
+ const starting = (async () => {
28441
+ await this.connectOrSpawn();
28442
+ const claim = await this.requestWithMessage({
28443
+ protocolVersion: 2,
28444
+ type: "claim",
28445
+ requestId: randomUUID5(),
28446
+ lease: this.freshLease(),
28447
+ config: this.config
28448
+ }, 15e3);
28449
+ const health = claim?.health;
28450
+ if (claim?.state !== "READY" || health?.schemaVersion !== 2 || health.authority !== "process-external-supervisor" || health.state !== "running" || health.ownerKey !== this.ownerHash || health.instanceId !== this.lease.instanceId || health.generation !== this.lease.generation || health.relayClientCount !== (this.config.relay?.enabled && this.config.relay.url ? 1 : 0) || !health.claimedAt) {
28451
+ throw new Error("OWNERSHIP_PRIMITIVE_UNAVAILABLE:supervisor claim did not return certified READY/CLAIMED health");
28452
+ }
28453
+ this.health = health;
28454
+ this.renewTimer = setInterval(() => this.renew(), SUPERVISOR_RENEW_MS);
28455
+ this.renewTimer.unref?.();
28456
+ this.logger.info(`[perkos-a2a] Supervisor owner ready generation=${this.lease.generation} instanceId=${this.lease.instanceId}`);
28457
+ })().finally(() => {
28458
+ if (this.startPromise === starting) this.startPromise = null;
28459
+ });
28460
+ this.startPromise = starting;
28461
+ return starting;
28462
+ }
28463
+ stop() {
28464
+ if (this.stopPromise) return this.stopPromise;
28465
+ if (this.stopped && !this.socket) return Promise.resolve();
28466
+ this.stopped = true;
28467
+ if (this.renewTimer) clearInterval(this.renewTimer);
28468
+ this.renewTimer = null;
28469
+ const stopping = (async () => {
28470
+ if (this.socket && !this.socket.destroyed) {
28471
+ const requestId = randomUUID5();
28472
+ try {
28473
+ await this.requestWithMessage({
28474
+ protocolVersion: 2,
28475
+ type: "release",
28476
+ requestId,
28477
+ lease: this.freshLease()
28478
+ }, 8e3);
28479
+ } catch (error) {
28480
+ const message = error instanceof Error ? error.message : String(error);
28481
+ if (!message.includes("NOT_OWNER") && !message.includes("SUPERVISOR_DISCONNECTED")) throw error;
28482
+ }
28483
+ }
28484
+ this.closeSocket(new Error("CLIENT_STOPPED"));
28485
+ this.logger.info(`[perkos-a2a] A2A server stopped generation=${this.lease.generation} instanceId=${this.lease.instanceId}`);
28486
+ })().finally(() => {
28487
+ if (this.stopPromise === stopping) this.stopPromise = null;
28488
+ });
28489
+ this.stopPromise = stopping;
28490
+ return stopping;
28491
+ }
28492
+ async stopAndWait() {
28493
+ await this.stop();
28494
+ return { closeTimedOut: false };
28495
+ }
28496
+ async sendTask(targetAgent, payload, taskId) {
28497
+ return await this.request("sendTask", { target: targetAgent, payload, ...taskId ? { taskId } : {} }, 7e4);
28498
+ }
28499
+ async discover() {
28500
+ return await this.request("discover", {}, 15e3);
28501
+ }
28502
+ sendTaskResponse(original, result) {
28503
+ this.write({ protocolVersion: 2, type: "relayTaskResult", lease: this.freshLease(), eventId: randomUUID5(), original, result });
28504
+ }
28505
+ async sendChatReply(payload) {
28506
+ return await this.request("chatReply", payload, 15e3);
28507
+ }
28508
+ async request(method, payload, timeoutMs) {
28509
+ const requestId = randomUUID5();
28510
+ return this.requestWithMessage({ protocolVersion: 2, type: "request", requestId, lease: this.freshLease(), method, payload }, timeoutMs);
28511
+ }
28512
+ async requestWithMessage(message, timeoutMs) {
28513
+ return new Promise((resolveRequest, rejectRequest) => {
28514
+ const timer = setTimeout(() => {
28515
+ this.pending.delete(message.requestId);
28516
+ rejectRequest(new Error(`SUPERVISOR_REQUEST_TIMEOUT:${message.type}`));
28517
+ }, timeoutMs);
28518
+ timer.unref?.();
28519
+ this.pending.set(message.requestId, { resolve: resolveRequest, reject: rejectRequest, timer });
28520
+ try {
28521
+ this.write(message);
28522
+ } catch (error) {
28523
+ clearTimeout(timer);
28524
+ this.pending.delete(message.requestId);
28525
+ rejectRequest(error instanceof Error ? error : new Error(String(error)));
28526
+ }
28527
+ });
28528
+ }
28529
+ freshLease() {
28530
+ this.lease.lastRenewedAt = (/* @__PURE__ */ new Date()).toISOString();
28531
+ return { ...this.lease };
28532
+ }
28533
+ renew() {
28534
+ if (this.stopped || !this.socket || this.socket.destroyed) return;
28535
+ this.write({ protocolVersion: 2, type: "renew", lease: this.freshLease() });
28536
+ }
28537
+ async connectOrSpawn() {
28538
+ try {
28539
+ await this.connectSocket(1e3);
28540
+ return;
28541
+ } catch {
28542
+ }
28543
+ const cliPath = this.supervisorCliPath ?? fileURLToPath(new URL("./openclaw-supervisor-cli.js", import.meta.url));
28544
+ if (!existsSync(cliPath)) {
28545
+ throw new Error(`OWNERSHIP_PRIMITIVE_UNAVAILABLE:supervisor CLI is missing at ${cliPath}`);
28546
+ }
28547
+ const bootstrapPath = supervisorBootstrapPath(this.stateDir, this.ownerHash);
28548
+ try {
28549
+ unlinkSync(bootstrapPath);
28550
+ } catch {
28551
+ }
28552
+ let childExit;
28553
+ const child = spawn(process.execPath, [cliPath, "--state-dir", this.stateDir, "--owner-key", this.ownerHash], {
28554
+ detached: true,
28555
+ stdio: "ignore",
28556
+ env: { ...process.env }
28557
+ });
28558
+ child.once("error", (error) => {
28559
+ childExit = `spawn error: ${error.message}`;
28560
+ });
28561
+ child.once("exit", (code, signal) => {
28562
+ childExit = `exit code=${code ?? "null"} signal=${signal ?? "none"}`;
28563
+ });
28564
+ child.unref();
28565
+ const deadline = Date.now() + 1e4;
28566
+ let lastError;
28567
+ while (Date.now() < deadline) {
28568
+ await new Promise((resolveWait) => setTimeout(resolveWait, 100));
28569
+ try {
28570
+ await this.connectSocket(1e3);
28571
+ return;
28572
+ } catch (error) {
28573
+ lastError = error;
28574
+ }
28575
+ const bootstrap = this.readBootstrapStatus(bootstrapPath);
28576
+ if (bootstrap?.state === "failed") {
28577
+ throw new Error(`OWNERSHIP_PRIMITIVE_UNAVAILABLE:${bootstrap.errorCode || "SUPERVISOR_BOOT_FAILED"}:${bootstrap.message || "supervisor failed before READY"}`);
28578
+ }
28579
+ if (childExit) throw new Error(`OWNERSHIP_PRIMITIVE_UNAVAILABLE:supervisor ${childExit}`);
28580
+ }
28581
+ throw new Error(`OWNERSHIP_PRIMITIVE_UNAVAILABLE:${lastError instanceof Error ? lastError.message : String(lastError)}`);
28582
+ }
28583
+ readBootstrapStatus(path) {
28584
+ try {
28585
+ const value = JSON.parse(readFileSync3(path, "utf8"));
28586
+ return value?.schemaVersion === 1 && value.ownerKey === this.ownerHash ? value : null;
28587
+ } catch {
28588
+ return null;
28589
+ }
28590
+ }
28591
+ connectSocket(timeoutMs) {
28592
+ this.closeSocket(new Error("SUPERVISOR_RECONNECT"));
28593
+ return new Promise((resolveConnect, rejectConnect) => {
28594
+ const socket = net.createConnection(this.socketPath);
28595
+ socket.setEncoding("utf8");
28596
+ let buffered = "";
28597
+ let settled = false;
28598
+ const timer = setTimeout(() => {
28599
+ if (settled) return;
28600
+ settled = true;
28601
+ socket.destroy();
28602
+ rejectConnect(new Error("supervisor socket connect timeout"));
28603
+ }, timeoutMs);
28604
+ timer.unref?.();
28605
+ socket.once("connect", () => {
28606
+ if (settled) return;
28607
+ settled = true;
28608
+ clearTimeout(timer);
28609
+ this.socket = socket;
28610
+ resolveConnect();
28611
+ });
28612
+ socket.once("error", (error) => {
28613
+ if (!settled) {
28614
+ settled = true;
28615
+ clearTimeout(timer);
28616
+ rejectConnect(error);
28617
+ }
28618
+ });
28619
+ socket.on("data", (chunk) => {
28620
+ buffered += chunk;
28621
+ for (; ; ) {
28622
+ const newline = buffered.indexOf("\n");
28623
+ if (newline < 0) break;
28624
+ const line = buffered.slice(0, newline);
28625
+ buffered = buffered.slice(newline + 1);
28626
+ if (line.trim()) void this.handleEvent(JSON.parse(line));
28627
+ }
28628
+ });
28629
+ socket.on("close", () => {
28630
+ if (this.socket === socket) this.closeSocket(new Error("SUPERVISOR_DISCONNECTED"));
28631
+ });
28632
+ });
28633
+ }
28634
+ async handleEvent(event) {
28635
+ if (event.protocolVersion !== SUPERVISOR_PROTOCOL_VERSION) return;
28636
+ if (event.type === "response") {
28637
+ const pending = this.pending.get(event.requestId);
28638
+ if (!pending) return;
28639
+ this.pending.delete(event.requestId);
28640
+ clearTimeout(pending.timer);
28641
+ if (event.ok) pending.resolve(event.result);
28642
+ else pending.reject(new Error(`${event.error?.code || "SUPERVISOR_ERROR"}:${event.error?.message || "request failed"}`));
28643
+ return;
28644
+ }
28645
+ if (event.type === "health") {
28646
+ this.health = event.health;
28647
+ return;
28648
+ }
28649
+ if (event.type === "log") {
28650
+ (event.level === "error" ? this.logger.error : this.logger.info)(event.message);
28651
+ return;
28652
+ }
28653
+ if (event.type === "relayTask") {
28654
+ try {
28655
+ const result = await this.handlers.onRelayTask(event.message);
28656
+ this.write({ protocolVersion: 2, type: "relayTaskResult", lease: this.freshLease(), eventId: event.eventId, original: event.message, result });
28657
+ } catch (error) {
28658
+ const message = error instanceof Error ? error.message : String(error);
28659
+ this.write({
28660
+ protocolVersion: 2,
28661
+ type: "relayTaskResult",
28662
+ lease: this.freshLease(),
28663
+ eventId: event.eventId,
28664
+ original: event.message,
28665
+ result: { jsonrpc: "2.0", id: event.message.id, error: { code: -32603, message } }
28666
+ });
28667
+ }
28668
+ return;
28669
+ }
28670
+ if (event.type === "chatDeliver") await this.handlers.onChatDeliver(event.frame);
28671
+ if (event.type === "channelJoin") await this.handlers.onChannelJoin(event.frame);
28672
+ }
28673
+ write(message) {
28674
+ if (!this.socket || this.socket.destroyed) throw new Error("SUPERVISOR_DISCONNECTED");
28675
+ this.socket.write(encodeSupervisorMessage(message));
28676
+ }
28677
+ closeSocket(error) {
28678
+ if (this.renewTimer) clearInterval(this.renewTimer);
28679
+ this.renewTimer = null;
28680
+ const socket = this.socket;
28681
+ this.socket = null;
28682
+ if (socket && !socket.destroyed) socket.destroy();
28683
+ for (const pending of this.pending.values()) {
28684
+ clearTimeout(pending.timer);
28685
+ pending.reject(error);
28686
+ }
28687
+ this.pending.clear();
28688
+ }
28689
+ };
28690
+
28691
+ // src/runtime-evidence.ts
28692
+ import { createHmac } from "node:crypto";
28693
+ import {
28694
+ chmodSync as chmodSync2,
28695
+ mkdirSync as mkdirSync2,
28696
+ readFileSync as readFileSync4,
28697
+ renameSync as renameSync2,
28698
+ writeFileSync as writeFileSync2
28699
+ } from "node:fs";
28700
+ import { homedir as homedir4 } from "node:os";
28701
+ import { join as join3, resolve as resolve2 } from "node:path";
28702
+ var RUNTIME_EVIDENCE_SCHEMA_VERSION = 2;
28703
+ function canonicalize(value) {
28704
+ if (Array.isArray(value)) return value.map(canonicalize);
28705
+ if (!value || typeof value !== "object") return value;
28706
+ return Object.fromEntries(
28707
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, child]) => [key, canonicalize(child)])
28708
+ );
28709
+ }
28710
+ function canonicalConfigJson(config) {
28711
+ return JSON.stringify({ schemaVersion: RUNTIME_EVIDENCE_SCHEMA_VERSION, config: canonicalize(config) });
28712
+ }
28713
+ function configFingerprint(config, hmacKeyHex) {
28714
+ if (!/^[a-f0-9]{64}$/u.test(hmacKeyHex)) throw new Error("runtime evidence HMAC key is invalid");
28715
+ return createHmac("sha256", Buffer.from(hmacKeyHex, "hex")).update(canonicalConfigJson(config)).digest("hex");
28716
+ }
28717
+ function runtimeEvidenceDir(stateDir) {
28718
+ return resolve2(stateDir ?? process.env.OPENCLAW_STATE_DIR ?? join3(homedir4(), ".openclaw"), "perkos-a2a");
28719
+ }
28720
+ function runtimeAttemptPath(stateDir) {
28721
+ return join3(runtimeEvidenceDir(stateDir), "active-attempt.json");
28722
+ }
28723
+ function runtimeStatusPath(stateDir) {
28724
+ return join3(runtimeEvidenceDir(stateDir), "runtime-status.json");
28725
+ }
28726
+ function configAgentId(config) {
28727
+ if (config.platform?.agentId) return config.platform.agentId;
28728
+ const match = config.platform?.heartbeatUrl?.match(/\/agents\/([^/]+)\/heartbeat/u);
28729
+ return match?.[1] ?? "unknown";
28730
+ }
28731
+ function recordRuntimeLoadEvidence(input) {
28732
+ const evidenceDir = runtimeEvidenceDir(input.stateDir);
28733
+ let attempt;
28734
+ try {
28735
+ attempt = JSON.parse(readFileSync4(runtimeAttemptPath(input.stateDir), "utf8"));
28736
+ } catch {
28737
+ input.logger.info(`[perkos-a2a] config loaded version=${input.version} agentId=${configAgentId(input.config)} attemptId=none`);
28738
+ return null;
28739
+ }
28740
+ const agentId = configAgentId(input.config);
28741
+ if (attempt.schemaVersion !== RUNTIME_EVIDENCE_SCHEMA_VERSION || attempt.targetVersion !== input.version || attempt.expectedAgentId !== agentId) {
28742
+ input.logger.warn?.("[perkos-a2a] runtime evidence attempt does not match the loaded plugin identity");
28743
+ return null;
28744
+ }
28745
+ const status = {
28746
+ schemaVersion: RUNTIME_EVIDENCE_SCHEMA_VERSION,
28747
+ loadedAt: (input.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
28748
+ version: input.version,
28749
+ agentId,
28750
+ fingerprint: configFingerprint(input.config, attempt.hmacKeyHex),
28751
+ attemptId: attempt.attemptId,
28752
+ pid: process.pid,
28753
+ processStartTime: processStartIdentity(),
28754
+ bootId: currentBootId(),
28755
+ pluginRoot: resolve2(input.pluginRoot)
28756
+ };
28757
+ mkdirSync2(evidenceDir, { recursive: true, mode: 448 });
28758
+ const target = runtimeStatusPath(input.stateDir);
28759
+ const temporary = `${target}.${process.pid}.tmp`;
28760
+ writeFileSync2(temporary, `${JSON.stringify(status, null, 2)}
28761
+ `, { mode: 384 });
28762
+ renameSync2(temporary, target);
28763
+ chmodSync2(target, 384);
28764
+ input.logger.info(
28765
+ `[perkos-a2a] config loaded version=${status.version} agentId=${status.agentId} attemptId=${status.attemptId} fingerprint=${status.fingerprint}`
28766
+ );
28767
+ return status;
28768
+ }
28769
+
28304
28770
  // src/chat-client.ts
28771
+ import { randomUUID as randomUUID6 } from "node:crypto";
28305
28772
  var DEFAULT_URL = "wss://chat.perkos.xyz/chat";
28306
28773
  var DEFAULT_HISTORY_LIMIT = 50;
28307
28774
  var DEFAULT_MIN_RECONNECT = 1e3;
@@ -28324,6 +28791,7 @@ var ChatClient = class {
28324
28791
  connected = false;
28325
28792
  authed = false;
28326
28793
  stopped = false;
28794
+ generation = 0;
28327
28795
  constructor(opts) {
28328
28796
  this.agentName = opts.agentName;
28329
28797
  const cfg = opts.config;
@@ -28359,20 +28827,43 @@ var ChatClient = class {
28359
28827
  return;
28360
28828
  }
28361
28829
  this.stopped = false;
28362
- this.connect();
28830
+ const generation = ++this.generation;
28831
+ this.connect(generation);
28363
28832
  }
28364
28833
  stop() {
28365
- this.stopped = true;
28366
- this.clearTimers();
28367
- if (this.ws) {
28834
+ const socket = this.beginStop();
28835
+ try {
28836
+ socket?.close(1e3, "client shutting down");
28837
+ } catch {
28838
+ }
28839
+ }
28840
+ async stopAndWait(timeoutMs = 5e3) {
28841
+ const socket = this.beginStop();
28842
+ if (!socket || socket.readyState === wrapper_default.CLOSED) return { closeTimedOut: false };
28843
+ return new Promise((resolve4) => {
28844
+ let settled = false;
28845
+ const finish = (closeTimedOut) => {
28846
+ if (settled) return;
28847
+ settled = true;
28848
+ clearTimeout(timer);
28849
+ resolve4({ closeTimedOut });
28850
+ };
28851
+ const timer = setTimeout(() => {
28852
+ try {
28853
+ socket.terminate();
28854
+ } catch {
28855
+ }
28856
+ finish(true);
28857
+ }, timeoutMs);
28858
+ timer.unref?.();
28859
+ socket.once("close", () => finish(false));
28860
+ socket.once("error", () => finish(false));
28368
28861
  try {
28369
- this.ws.close(1e3, "client shutting down");
28862
+ socket.close(1e3, "client shutting down");
28370
28863
  } catch {
28864
+ finish(false);
28371
28865
  }
28372
- this.ws = null;
28373
- }
28374
- this.connected = false;
28375
- this.authed = false;
28866
+ });
28376
28867
  }
28377
28868
  /**
28378
28869
  * Send a reply from this agent into a conversation. Appends to the local
@@ -28382,7 +28873,7 @@ var ChatClient = class {
28382
28873
  * required (the simple agent loop should just regenerate from history).
28383
28874
  */
28384
28875
  async sendReply(opts) {
28385
- const id = randomUUID5();
28876
+ const id = randomUUID6();
28386
28877
  const msg = {
28387
28878
  id,
28388
28879
  from: `agent:${this.agentName}`,
@@ -28405,27 +28896,32 @@ var ChatClient = class {
28405
28896
  // -------------------------------------------------------------------------
28406
28897
  // WebSocket lifecycle
28407
28898
  // -------------------------------------------------------------------------
28408
- connect() {
28409
- if (this.stopped) return;
28899
+ connect(generation) {
28900
+ if (!this.isCurrent(generation)) return;
28410
28901
  const url = this.config.url;
28411
28902
  this.logger.info(`[perkos-chat] connecting to ${url}`);
28412
28903
  try {
28413
28904
  this.ws = new wrapper_default(url);
28414
28905
  } catch (err) {
28415
28906
  this.logger.error(`[perkos-chat] failed to construct WS: ${errMsg(err)}`);
28416
- this.scheduleReconnect();
28907
+ this.scheduleReconnect(generation);
28417
28908
  return;
28418
28909
  }
28910
+ const socket = this.ws;
28419
28911
  let authTimer = setTimeout(() => {
28420
- if (!this.authed) {
28912
+ if (this.isCurrent(generation, socket) && !this.authed) {
28421
28913
  this.logger.error("[perkos-chat] auth timeout");
28422
28914
  try {
28423
- this.ws?.close(4002, "auth timeout");
28915
+ socket.close(4002, "auth timeout");
28424
28916
  } catch {
28425
28917
  }
28426
28918
  }
28427
28919
  }, AUTH_TIMEOUT_MS);
28428
28920
  this.ws.on("open", () => {
28921
+ if (!this.isCurrent(generation, socket)) {
28922
+ socket.close(1e3, "stale chat generation");
28923
+ return;
28924
+ }
28429
28925
  this.connected = true;
28430
28926
  this.reconnectMs = this.config.minReconnectMs;
28431
28927
  this.sendFrame({
@@ -28436,6 +28932,7 @@ var ChatClient = class {
28436
28932
  });
28437
28933
  });
28438
28934
  this.ws.on("message", (data) => {
28935
+ if (!this.isCurrent(generation, socket)) return;
28439
28936
  let frame;
28440
28937
  try {
28441
28938
  frame = JSON.parse(data.toString("utf8"));
@@ -28450,19 +28947,21 @@ var ChatClient = class {
28450
28947
  void this.handleFrame(frame);
28451
28948
  });
28452
28949
  this.ws.on("close", (code, reason) => {
28453
- this.connected = false;
28454
- this.authed = false;
28455
- this.clearTimers();
28456
28950
  if (authTimer) {
28457
28951
  clearTimeout(authTimer);
28458
28952
  authTimer = null;
28459
28953
  }
28954
+ if (!this.isCurrent(generation, socket)) return;
28955
+ this.connected = false;
28956
+ this.authed = false;
28957
+ this.clearTimers();
28460
28958
  if (!this.stopped) {
28461
28959
  this.logger.info(`[perkos-chat] disconnected (${code}: ${reason?.toString() || "no reason"}); reconnecting`);
28462
- this.scheduleReconnect();
28960
+ this.scheduleReconnect(generation);
28463
28961
  }
28464
28962
  });
28465
28963
  this.ws.on("error", (err) => {
28964
+ if (!this.isCurrent(generation, socket)) return;
28466
28965
  this.logger.error(`[perkos-chat] WS error: ${errMsg(err)}`);
28467
28966
  });
28468
28967
  }
@@ -28651,10 +29150,12 @@ var ChatClient = class {
28651
29150
  }
28652
29151
  }, this.config.heartbeatIntervalMs);
28653
29152
  }
28654
- scheduleReconnect() {
28655
- if (this.stopped) return;
29153
+ scheduleReconnect(generation) {
29154
+ if (!this.isCurrent(generation)) return;
28656
29155
  const delay = this.reconnectMs;
28657
- this.reconnectTimer = setTimeout(() => this.connect(), delay);
29156
+ this.reconnectTimer = setTimeout(() => {
29157
+ if (this.isCurrent(generation)) this.connect(generation);
29158
+ }, delay);
28658
29159
  this.reconnectMs = Math.min(delay * 2, this.config.maxReconnectMs);
28659
29160
  }
28660
29161
  clearTimers() {
@@ -28667,151 +29168,25 @@ var ChatClient = class {
28667
29168
  this.reconnectTimer = null;
28668
29169
  }
28669
29170
  }
29171
+ isCurrent(generation, socket) {
29172
+ return !this.stopped && generation === this.generation && (!socket || socket === this.ws);
29173
+ }
29174
+ beginStop() {
29175
+ if (this.stopped && !this.ws) return null;
29176
+ this.stopped = true;
29177
+ this.generation += 1;
29178
+ this.clearTimers();
29179
+ const socket = this.ws;
29180
+ this.ws = null;
29181
+ this.connected = false;
29182
+ this.authed = false;
29183
+ return socket;
29184
+ }
28670
29185
  };
28671
29186
  function errMsg(err) {
28672
29187
  return err instanceof Error ? err.message : String(err);
28673
29188
  }
28674
29189
 
28675
- // src/platform-heartbeat.ts
28676
- var DEFAULT_INTERVAL_MS = 6e4;
28677
- var MIN_INTERVAL_MS = 1e4;
28678
- function startPlatformHeartbeat(opts) {
28679
- const env = opts.env ?? process.env;
28680
- const fetcher = opts.fetcher ?? fetch;
28681
- const url = opts.url?.trim() || env.PERKOS_HEARTBEAT_URL?.trim();
28682
- const relayKey = opts.relayKey?.trim() || env.A2A_RELAY_API_KEY?.trim();
28683
- if (!url || !relayKey) return { stop: () => void 0 };
28684
- const configuredInterval = Number(
28685
- opts.intervalMs ?? env.PERKOS_HEARTBEAT_INTERVAL_MS ?? DEFAULT_INTERVAL_MS
28686
- );
28687
- const intervalMs = Number.isFinite(configuredInterval) ? Math.max(MIN_INTERVAL_MS, configuredInterval) : DEFAULT_INTERVAL_MS;
28688
- const a2aRuntime = env.A2A_RUNTIME?.trim() || "hermes-api";
28689
- const runtimeKind = opts.runtimeKind ?? (a2aRuntime === "openclaw" ? "openclaw" : a2aRuntime === "custom" ? "custom" : "hermes");
28690
- let stopped = false;
28691
- let firstSuccess = true;
28692
- const report = async () => {
28693
- if (stopped) return;
28694
- try {
28695
- const res = await fetcher(url, {
28696
- method: "POST",
28697
- headers: {
28698
- "content-type": "application/json",
28699
- authorization: `Bearer ${relayKey}`,
28700
- "x-relay-key": relayKey
28701
- },
28702
- body: JSON.stringify({
28703
- runtimeKind,
28704
- version: opts.version?.trim() || env.PERKOS_A2A_VERSION?.trim() || "0.12.26",
28705
- ts: Date.now()
28706
- })
28707
- });
28708
- if (res.ok) {
28709
- if (firstSuccess) {
28710
- opts.logger.info(
28711
- `[perkos-heartbeat] reported online to ${url} (status=${res.status}, every=${intervalMs}ms)`
28712
- );
28713
- firstSuccess = false;
28714
- }
28715
- } else {
28716
- opts.logger.warn(
28717
- `[perkos-heartbeat] ${res.status} from ${url} (continuing)`
28718
- );
28719
- }
28720
- } catch (err) {
28721
- const msg = err instanceof Error ? err.message : String(err);
28722
- opts.logger.warn(
28723
- `[perkos-heartbeat] POST failed to ${url}: ${msg} (continuing)`
28724
- );
28725
- }
28726
- };
28727
- void report();
28728
- const timer = setInterval(() => void report(), intervalMs);
28729
- timer.unref?.();
28730
- return {
28731
- stop: () => {
28732
- stopped = true;
28733
- clearInterval(timer);
28734
- }
28735
- };
28736
- }
28737
-
28738
- // src/runtime-evidence.ts
28739
- import { createHmac } from "node:crypto";
28740
- import {
28741
- chmodSync,
28742
- mkdirSync,
28743
- readFileSync as readFileSync2,
28744
- renameSync,
28745
- writeFileSync
28746
- } from "node:fs";
28747
- import { homedir as homedir3 } from "node:os";
28748
- import { join as join2, resolve } from "node:path";
28749
- var RUNTIME_EVIDENCE_SCHEMA_VERSION = 1;
28750
- function canonicalize(value) {
28751
- if (Array.isArray(value)) return value.map(canonicalize);
28752
- if (!value || typeof value !== "object") return value;
28753
- return Object.fromEntries(
28754
- Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, child]) => [key, canonicalize(child)])
28755
- );
28756
- }
28757
- function canonicalConfigJson(config) {
28758
- return JSON.stringify({ schemaVersion: RUNTIME_EVIDENCE_SCHEMA_VERSION, config: canonicalize(config) });
28759
- }
28760
- function configFingerprint(config, hmacKeyHex) {
28761
- if (!/^[a-f0-9]{64}$/u.test(hmacKeyHex)) throw new Error("runtime evidence HMAC key is invalid");
28762
- return createHmac("sha256", Buffer.from(hmacKeyHex, "hex")).update(canonicalConfigJson(config)).digest("hex");
28763
- }
28764
- function runtimeEvidenceDir(stateDir) {
28765
- return resolve(stateDir ?? process.env.OPENCLAW_STATE_DIR ?? join2(homedir3(), ".openclaw"), "perkos-a2a");
28766
- }
28767
- function runtimeAttemptPath(stateDir) {
28768
- return join2(runtimeEvidenceDir(stateDir), "active-attempt.json");
28769
- }
28770
- function runtimeStatusPath(stateDir) {
28771
- return join2(runtimeEvidenceDir(stateDir), "runtime-status.json");
28772
- }
28773
- function configAgentId(config) {
28774
- if (config.platform?.agentId) return config.platform.agentId;
28775
- const match = config.platform?.heartbeatUrl?.match(/\/agents\/([^/]+)\/heartbeat/u);
28776
- return match?.[1] ?? "unknown";
28777
- }
28778
- function recordRuntimeLoadEvidence(input) {
28779
- const evidenceDir = runtimeEvidenceDir(input.stateDir);
28780
- let attempt;
28781
- try {
28782
- attempt = JSON.parse(readFileSync2(runtimeAttemptPath(input.stateDir), "utf8"));
28783
- } catch {
28784
- input.logger.info(`[perkos-a2a] config loaded version=${input.version} agentId=${configAgentId(input.config)} attemptId=none`);
28785
- return null;
28786
- }
28787
- const agentId = configAgentId(input.config);
28788
- if (attempt.schemaVersion !== RUNTIME_EVIDENCE_SCHEMA_VERSION || attempt.targetVersion !== input.version || attempt.expectedAgentId !== agentId) {
28789
- input.logger.warn?.("[perkos-a2a] runtime evidence attempt does not match the loaded plugin identity");
28790
- return null;
28791
- }
28792
- const status = {
28793
- schemaVersion: RUNTIME_EVIDENCE_SCHEMA_VERSION,
28794
- loadedAt: (input.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
28795
- version: input.version,
28796
- agentId,
28797
- fingerprint: configFingerprint(input.config, attempt.hmacKeyHex),
28798
- attemptId: attempt.attemptId,
28799
- pid: process.pid,
28800
- pluginRoot: resolve(input.pluginRoot)
28801
- };
28802
- mkdirSync(evidenceDir, { recursive: true, mode: 448 });
28803
- const target = runtimeStatusPath(input.stateDir);
28804
- const temporary = `${target}.${process.pid}.tmp`;
28805
- writeFileSync(temporary, `${JSON.stringify(status, null, 2)}
28806
- `, { mode: 384 });
28807
- renameSync(temporary, target);
28808
- chmodSync(target, 384);
28809
- input.logger.info(
28810
- `[perkos-a2a] config loaded version=${status.version} agentId=${status.agentId} attemptId=${status.attemptId} fingerprint=${status.fingerprint}`
28811
- );
28812
- return status;
28813
- }
28814
-
28815
29190
  // src/types.ts
28816
29191
  function isWebhookEvent(msg) {
28817
29192
  return msg.type === "webhook_event";
@@ -28820,7 +29195,7 @@ function isWebhookEvent(msg) {
28820
29195
  // src/index.ts
28821
29196
  function loadedPluginVersion() {
28822
29197
  try {
28823
- const manifest = JSON.parse(readFileSync3(new URL("../openclaw.plugin.json", import.meta.url), "utf8"));
29198
+ const manifest = JSON.parse(readFileSync5(new URL("../openclaw.plugin.json", import.meta.url), "utf8"));
28824
29199
  return typeof manifest.version === "string" ? manifest.version : "unknown";
28825
29200
  } catch {
28826
29201
  return "unknown";
@@ -28915,7 +29290,7 @@ async function prepareOpenClawChatSession(api, config, sessionKey, logger) {
28915
29290
  try {
28916
29291
  await patchSessionEntry({
28917
29292
  sessionKey,
28918
- fallbackEntry: { sessionId: randomUUID6(), updatedAt: Date.now() },
29293
+ fallbackEntry: { sessionId: randomUUID7(), updatedAt: Date.now() },
28919
29294
  preserveActivity: true,
28920
29295
  replaceEntry: true,
28921
29296
  update: (entry) => alignOpenClawChatSessionModel(entry, config.runtime?.model)
@@ -28935,7 +29310,7 @@ async function forceNativeOpenClawChatRuntime(api, config, sessionKey, logger) {
28935
29310
  try {
28936
29311
  await patchSessionEntry({
28937
29312
  sessionKey,
28938
- fallbackEntry: { sessionId: randomUUID6(), updatedAt: Date.now() },
29313
+ fallbackEntry: { sessionId: randomUUID7(), updatedAt: Date.now() },
28939
29314
  preserveActivity: true,
28940
29315
  replaceEntry: true,
28941
29316
  update: (entry) => ({
@@ -28976,7 +29351,7 @@ async function runOpenClawChatTurn(api, config, sessionKey, prompt, transcriptPr
28976
29351
  const runtimeConfig = currentConfig();
28977
29352
  const workspaceDir = resolveAgentWorkspaceDir(runtimeConfig, agentId);
28978
29353
  const entry = api.runtime?.agent?.session?.getSessionEntry?.({ agentId, sessionKey });
28979
- const sessionId = typeof entry?.sessionId === "string" && entry.sessionId.trim() ? entry.sessionId : randomUUID6();
29354
+ const sessionId = typeof entry?.sessionId === "string" && entry.sessionId.trim() ? entry.sessionId : randomUUID7();
28980
29355
  const configuredTimeout = api.runtime?.agent?.resolveAgentTimeoutMs?.(runtimeConfig);
28981
29356
  const timeoutMs = Math.min(
28982
29357
  75e3,
@@ -28991,7 +29366,7 @@ async function runOpenClawChatTurn(api, config, sessionKey, prompt, transcriptPr
28991
29366
  prompt,
28992
29367
  transcriptPrompt,
28993
29368
  timeoutMs,
28994
- runId: randomUUID6(),
29369
+ runId: randomUUID7(),
28995
29370
  trigger: "user",
28996
29371
  messageChannel: "perkos-chat",
28997
29372
  disableTools: true,
@@ -29077,11 +29452,6 @@ async function deliverToHermes(config, message, logger) {
29077
29452
  }
29078
29453
  logger.info(`[perkos-a2a] Delivered inbound A2A task to Hermes API session ${sessionKey} via ${url}`);
29079
29454
  }
29080
- var A2A_LIFECYCLE_REGISTRY = /* @__PURE__ */ Symbol.for("@perkos/perkos-a2a:lifecycle-registry");
29081
- function lifecycleRegistry() {
29082
- const root = globalThis;
29083
- return root[A2A_LIFECYCLE_REGISTRY] ??= /* @__PURE__ */ new Map();
29084
- }
29085
29455
  function a2aLifecycleKey(config) {
29086
29456
  const rawRelayUrl = (config.relay?.url || "no-relay").trim();
29087
29457
  let relayUrl = rawRelayUrl.replace(/[?#].*$/u, "").replace(/\/+$/u, "").toLowerCase();
@@ -29111,12 +29481,12 @@ function register(api) {
29111
29481
  recordRuntimeLoadEvidence({
29112
29482
  config: pluginConfig,
29113
29483
  version: pluginVersion,
29114
- pluginRoot: resolve2(dirname(fileURLToPath(import.meta.url)), ".."),
29484
+ pluginRoot: resolve3(dirname2(fileURLToPath2(import.meta.url)), ".."),
29115
29485
  logger
29116
29486
  });
29117
- const server = new A2AServer(pluginConfig, logger);
29487
+ const server = new A2AServer(pluginConfig, logger, { relayOwnership: "external-required" });
29118
29488
  const lifecycleKey = a2aLifecycleKey(pluginConfig);
29119
- const lifecycleInstanceId = randomUUID6();
29489
+ const lifecycleInstanceId = randomUUID7();
29120
29490
  const pendingTasks = [];
29121
29491
  const enqueueSystemEvent = api.runtime?.system?.enqueueSystemEvent;
29122
29492
  const requestHeartbeatNow = api.runtime?.system?.requestHeartbeatNow;
@@ -29171,7 +29541,7 @@ function register(api) {
29171
29541
  await deliverToHermes(pluginConfig, eventText, logger);
29172
29542
  task.artifacts.push({
29173
29543
  kind: "artifact",
29174
- artifactId: randomUUID6(),
29544
+ artifactId: randomUUID7(),
29175
29545
  parts: [{ kind: "text", text: `debug: delivered task ${task.id} to Hermes API Server` }]
29176
29546
  });
29177
29547
  const finalText2 = await generateRuntimeReply(pluginConfig, text, marker);
@@ -29181,14 +29551,14 @@ function register(api) {
29181
29551
  if (runtimeKind === "none") {
29182
29552
  task.artifacts.push({
29183
29553
  kind: "artifact",
29184
- artifactId: randomUUID6(),
29554
+ artifactId: randomUUID7(),
29185
29555
  parts: [{ kind: "text", text: `debug: runtime delivery disabled for ${marker}` }]
29186
29556
  });
29187
29557
  return;
29188
29558
  }
29189
29559
  task.artifacts.push({
29190
29560
  kind: "artifact",
29191
- artifactId: randomUUID6(),
29561
+ artifactId: randomUUID7(),
29192
29562
  parts: [{ kind: "text", text: `debug: enqueued system event for ${marker}` }]
29193
29563
  });
29194
29564
  if (enqueueSystemEvent) {
@@ -29272,7 +29642,7 @@ function register(api) {
29272
29642
  if (!finalText) continue;
29273
29643
  task.artifacts.push({
29274
29644
  kind: "artifact",
29275
- artifactId: randomUUID6(),
29645
+ artifactId: randomUUID7(),
29276
29646
  parts: [{ kind: "text", text: finalText }]
29277
29647
  });
29278
29648
  task.status = {
@@ -29286,179 +29656,145 @@ function register(api) {
29286
29656
  logger.info(`[perkos-a2a] Task ${task.id} completed from transcript update`);
29287
29657
  }
29288
29658
  });
29289
- api.registerService({
29290
- id: "perkos-a2a",
29291
- start: async () => {
29292
- const registry = lifecycleRegistry();
29293
- const previous = registry.get(lifecycleKey);
29294
- if (previous && previous.instanceId !== lifecycleInstanceId) {
29295
- logger.info(`[perkos-a2a] Waiting for previous relay generation to stop for ${pluginConfig.agentName}`);
29296
- await previous.server.stop({ reason: "replaced", timeoutMs: 5e3, drainMs: 3e3 });
29297
- }
29298
- registry.set(lifecycleKey, { instanceId: lifecycleInstanceId, server });
29299
- await server.start();
29300
- logger.info(`[perkos-a2a] A2A server started for ${pluginConfig.agentName}`);
29301
- },
29302
- stop: async () => {
29303
- logger.info("[perkos-a2a] A2A server stopping");
29304
- await server.stop({ reason: "openclaw-service-stop", timeoutMs: 5e3, drainMs: 3e3 });
29305
- const registry = lifecycleRegistry();
29306
- if (registry.get(lifecycleKey)?.instanceId === lifecycleInstanceId) registry.delete(lifecycleKey);
29307
- }
29308
- });
29309
29659
  const chatApiKey = pluginConfig.chat?.apiKey || pluginConfig.relay?.apiKey;
29310
- const chatClient = pluginConfig.chat?.enabled && chatApiKey ? new ChatClient({
29311
- agentName: pluginConfig.agentName,
29312
- config: {
29313
- enabled: true,
29314
- url: pluginConfig.chat?.url || "wss://chat.perkos.xyz/chat",
29315
- apiKey: chatApiKey,
29316
- storeRoot: pluginConfig.chat?.storeRoot,
29317
- defaultHistoryLimit: pluginConfig.chat?.defaultHistoryLimit,
29318
- minReconnectMs: pluginConfig.chat?.minReconnectMs,
29319
- maxReconnectMs: pluginConfig.chat?.maxReconnectMs,
29320
- heartbeatIntervalMs: pluginConfig.chat?.heartbeatIntervalMs
29321
- },
29322
- handlers: {
29323
- logger,
29324
- onChatDeliver: async (frame) => {
29325
- const walletAddress = parseWalletFromIdentity(frame.from);
29326
- const marker = `[PERKOS_CHAT:${frame.convId}]`;
29327
- const recentPage = await chatClient?.getStore().readPage(frame.convId, {
29328
- limit: CHAT_CONTEXT_MESSAGE_LIMIT + 1
29660
+ const chatEnabled = Boolean(pluginConfig.chat?.enabled && chatApiKey);
29661
+ const chatStore = chatEnabled ? new ChatStore({ storeRoot: pluginConfig.chat?.storeRoot }) : null;
29662
+ let supervisorClient;
29663
+ const handleSupervisorChatDeliver = async (frame) => {
29664
+ const walletAddress = parseWalletFromIdentity(frame.from);
29665
+ const marker = `[PERKOS_CHAT:${frame.convId}]`;
29666
+ const recentPage = await chatStore?.readPage(frame.convId, {
29667
+ limit: CHAT_CONTEXT_MESSAGE_LIMIT + 1
29668
+ });
29669
+ const recentContext = formatRecentChatContext(recentPage?.messages ?? [], frame.id);
29670
+ const eventText = [
29671
+ marker,
29672
+ `From: ${frame.from}`,
29673
+ `Conversation: ${frame.convId}`,
29674
+ frame.projectId ? `Canonical projectId: ${frame.projectId}` : "",
29675
+ walletAddress ? `Wallet: ${walletAddress}` : "",
29676
+ "",
29677
+ "A user is messaging you in a PerkOS chat conversation.",
29678
+ "To respond, call the `perkos_chat_reply` tool with:",
29679
+ ` convId: "${frame.convId}"`,
29680
+ walletAddress ? ` walletAddress: "${walletAddress}"` : ` walletAddress: <derived from From>`,
29681
+ ` text: <your reply>`,
29682
+ "",
29683
+ "Recent conversation context (oldest to newest):",
29684
+ recentContext,
29685
+ "",
29686
+ "Use the recent context. Do not ask again for information the user already provided.",
29687
+ "",
29688
+ "Message body:",
29689
+ frame.text
29690
+ ].filter(Boolean).join("\n");
29691
+ const directRunPrompt = [
29692
+ marker,
29693
+ `From: ${frame.from}`,
29694
+ `Conversation: ${frame.convId}`,
29695
+ frame.projectId ? `Canonical projectId: ${frame.projectId}` : "",
29696
+ "",
29697
+ "Reply to this PerkOS chat message using your current runtime configuration.",
29698
+ "Return only the reply text. Do not call messaging tools; the PerkOS plugin delivers the returned text.",
29699
+ "Use the recent context and do not ask again for information already provided.",
29700
+ "",
29701
+ "Recent conversation context (oldest to newest):",
29702
+ recentContext,
29703
+ "",
29704
+ "Message body:",
29705
+ frame.text
29706
+ ].filter(Boolean).join("\n");
29707
+ const runtimeKind = pluginConfig.runtime?.kind || "openclaw";
29708
+ if (runtimeKind === "hermes" || runtimeKind === "hermes-api") {
29709
+ try {
29710
+ await deliverToHermes(pluginConfig, eventText, logger);
29711
+ } catch (err) {
29712
+ logger.error(`[perkos-chat] hermes delivery failed: ${err instanceof Error ? err.message : String(err)}`);
29713
+ }
29714
+ return;
29715
+ }
29716
+ if (runtimeKind === "none") {
29717
+ logger.info(`[perkos-chat] dropped chat_deliver for ${frame.convId} (runtime=none)`);
29718
+ return;
29719
+ }
29720
+ const chatSessionKey = openclawChatSessionKey(pluginConfig, frame.convId);
29721
+ await prepareOpenClawChatSession(api, pluginConfig, chatSessionKey, logger);
29722
+ if (walletAddress) {
29723
+ const reply = await runOpenClawChatTurn(api, pluginConfig, chatSessionKey, directRunPrompt, frame.text, logger);
29724
+ if (reply) {
29725
+ const result = await supervisorClient.sendChatReply({
29726
+ convId: frame.convId,
29727
+ walletAddress,
29728
+ text: reply,
29729
+ replyTo: frame.id
29329
29730
  });
29330
- const recentContext = formatRecentChatContext(
29331
- recentPage?.messages ?? [],
29332
- frame.id
29333
- );
29334
- const eventText = [
29335
- marker,
29336
- `From: ${frame.from}`,
29337
- `Conversation: ${frame.convId}`,
29338
- frame.projectId ? `Canonical projectId: ${frame.projectId}` : "",
29339
- walletAddress ? `Wallet: ${walletAddress}` : "",
29340
- "",
29341
- "A user is messaging you in a PerkOS chat conversation.",
29342
- "To respond, call the `perkos_chat_reply` tool with:",
29343
- ` convId: "${frame.convId}"`,
29344
- walletAddress ? ` walletAddress: "${walletAddress}"` : ` walletAddress: <derived from From>`,
29345
- ` text: <your reply>`,
29346
- "",
29347
- "Recent conversation context (oldest to newest):",
29348
- recentContext,
29349
- "",
29350
- "Use the recent context. Do not ask again for information the user already provided.",
29351
- "",
29352
- "Message body:",
29353
- frame.text
29354
- ].filter(Boolean).join("\n");
29355
- const directRunPrompt = [
29356
- marker,
29357
- `From: ${frame.from}`,
29358
- `Conversation: ${frame.convId}`,
29359
- frame.projectId ? `Canonical projectId: ${frame.projectId}` : "",
29360
- "",
29361
- "Reply to this PerkOS chat message using your current runtime configuration.",
29362
- "Return only the reply text. Do not call messaging tools; the PerkOS plugin delivers the returned text.",
29363
- "Use the recent context and do not ask again for information already provided.",
29364
- "",
29365
- "Recent conversation context (oldest to newest):",
29366
- recentContext,
29367
- "",
29368
- "Message body:",
29369
- frame.text
29370
- ].filter(Boolean).join("\n");
29371
- const runtimeKind = pluginConfig.runtime?.kind || "openclaw";
29372
- if (runtimeKind === "hermes" || runtimeKind === "hermes-api") {
29373
- try {
29374
- await deliverToHermes(pluginConfig, eventText, logger);
29375
- } catch (err) {
29376
- logger.error(`[perkos-chat] hermes delivery failed: ${err instanceof Error ? err.message : String(err)}`);
29377
- }
29378
- return;
29379
- }
29380
- if (runtimeKind === "none") {
29381
- logger.info(`[perkos-chat] dropped chat_deliver for ${frame.convId} (runtime=none)`);
29382
- return;
29383
- }
29384
- const chatSessionKey = openclawChatSessionKey(pluginConfig, frame.convId);
29385
- await prepareOpenClawChatSession(api, pluginConfig, chatSessionKey, logger);
29386
- if (walletAddress) {
29387
- const reply = await runOpenClawChatTurn(
29388
- api,
29389
- pluginConfig,
29390
- chatSessionKey,
29391
- directRunPrompt,
29392
- frame.text,
29393
- logger
29394
- );
29395
- if (reply) {
29396
- const result = await chatClient?.sendReply({
29397
- convId: frame.convId,
29398
- walletAddress,
29399
- text: reply,
29400
- replyTo: frame.id
29401
- });
29402
- logger.info(
29403
- `[perkos-chat] embedded reply ${result?.delivered ? "delivered" : "queued (offline)"} for ${frame.convId}`
29404
- );
29405
- return;
29406
- }
29407
- }
29408
- if (enqueueSystemEvent) {
29409
- enqueueSystemEvent(eventText, { sessionKey: chatSessionKey });
29410
- }
29411
- wakeGatewayAgent(
29412
- requestHeartbeatNow,
29413
- `[PerkOS-Chat] inbound message in ${frame.convId}`,
29414
- logger,
29415
- chatSessionKey
29416
- );
29417
- },
29418
- onChannelJoin: async (frame) => {
29419
- logger.info(`[perkos-chat] joined conv ${frame.convId} (host: ${frame.historyHost}, ${frame.participants.length} participants)`);
29731
+ logger.info(`[perkos-chat] embedded reply ${result.delivered ? "delivered" : "queued (offline)"} for ${frame.convId}`);
29732
+ return;
29420
29733
  }
29421
29734
  }
29422
- }) : null;
29735
+ if (enqueueSystemEvent) enqueueSystemEvent(eventText, { sessionKey: chatSessionKey });
29736
+ wakeGatewayAgent(requestHeartbeatNow, `[PerkOS-Chat] inbound message in ${frame.convId}`, logger, chatSessionKey);
29737
+ };
29423
29738
  if (pluginConfig.chat?.enabled && !chatApiKey) {
29424
29739
  logger.error("[perkos-chat] chat.enabled=true but no apiKey (set chat.apiKey or fall back from relay.apiKey)");
29425
29740
  }
29426
- if (chatClient) {
29427
- api.registerService({
29428
- id: "perkos-a2a-chat",
29429
- start: () => {
29430
- chatClient.start();
29431
- logger.info(`[perkos-chat] client started for ${pluginConfig.agentName}`);
29432
- },
29433
- stop: () => {
29434
- chatClient.stop();
29435
- logger.info("[perkos-chat] client stopped");
29741
+ const platformAgentId = pluginConfig.platform?.agentId || pluginConfig.platform?.heartbeatUrl?.match(/\/agents\/([^/]+)\/heartbeat/u)?.[1] || pluginConfig.agentName.trim().toLowerCase();
29742
+ supervisorClient = new OpenClawSupervisorClient({
29743
+ ownerHash: supervisorOwnerHash(lifecycleKey),
29744
+ generation: Date.now(),
29745
+ instanceId: lifecycleInstanceId,
29746
+ config: {
29747
+ agentName: pluginConfig.agentName,
29748
+ agentId: platformAgentId,
29749
+ version: pluginVersion,
29750
+ relay: pluginConfig.relay,
29751
+ chat: chatEnabled ? {
29752
+ enabled: true,
29753
+ url: pluginConfig.chat?.url || "wss://chat.perkos.xyz/chat",
29754
+ apiKey: chatApiKey || "",
29755
+ storeRoot: pluginConfig.chat?.storeRoot,
29756
+ defaultHistoryLimit: pluginConfig.chat?.defaultHistoryLimit,
29757
+ minReconnectMs: pluginConfig.chat?.minReconnectMs,
29758
+ maxReconnectMs: pluginConfig.chat?.maxReconnectMs,
29759
+ heartbeatIntervalMs: pluginConfig.chat?.heartbeatIntervalMs
29760
+ } : void 0,
29761
+ platform: {
29762
+ heartbeatUrl: pluginConfig.platform?.heartbeatUrl,
29763
+ heartbeatIntervalMs: pluginConfig.platform?.heartbeatIntervalMs
29764
+ },
29765
+ agentCard: server.getAgentCard()
29766
+ },
29767
+ logger,
29768
+ handlers: {
29769
+ onRelayTask: (message) => server.handleTransportTask(message),
29770
+ onChatDeliver: handleSupervisorChatDeliver,
29771
+ onChannelJoin: async (frame) => {
29772
+ logger.info(`[perkos-chat] joined conv ${frame.convId} (host: ${frame.historyHost}, ${frame.participants.length} participants)`);
29436
29773
  }
29437
- });
29438
- }
29439
- if (pluginConfig.platform?.heartbeatUrl) {
29440
- let platformHeartbeat = null;
29441
- api.registerService({
29442
- id: "perkos-a2a-platform-heartbeat",
29443
- start: () => {
29444
- platformHeartbeat = startPlatformHeartbeat({
29445
- logger: {
29446
- info: (message) => logger.info(message),
29447
- warn: (message) => (logger.warn || logger.info).call(logger, message)
29448
- },
29449
- url: pluginConfig.platform?.heartbeatUrl,
29450
- relayKey: pluginConfig.relay?.apiKey,
29451
- runtimeKind: "openclaw",
29452
- version: pluginVersion,
29453
- intervalMs: pluginConfig.platform?.heartbeatIntervalMs
29454
- });
29455
- },
29456
- stop: () => {
29457
- platformHeartbeat?.stop();
29458
- platformHeartbeat = null;
29774
+ }
29775
+ });
29776
+ server.setRelayTransport(supervisorClient);
29777
+ api.registerService({
29778
+ id: "perkos-a2a",
29779
+ start: async () => {
29780
+ try {
29781
+ await supervisorClient.start();
29782
+ await server.start();
29783
+ logger.info(`[perkos-a2a] A2A server started for ${pluginConfig.agentName}`);
29784
+ } catch (error) {
29785
+ await server.stop({ reason: "openclaw-service-start-failed", timeoutMs: 2e3, drainMs: 0 }).catch(() => void 0);
29786
+ await supervisorClient.stop().catch(() => void 0);
29787
+ const message = error instanceof Error ? error.message : String(error);
29788
+ logger.error(`[perkos-a2a] OWNERSHIP_START_FAILED:${message}`);
29789
+ throw error;
29459
29790
  }
29460
- });
29461
- }
29791
+ },
29792
+ stop: async () => {
29793
+ logger.info("[perkos-a2a] A2A server stopping");
29794
+ await server.stop({ reason: "openclaw-service-stop", timeoutMs: 8e3, drainMs: 3e3 });
29795
+ await supervisorClient.stop();
29796
+ }
29797
+ });
29462
29798
  api.registerTool({
29463
29799
  name: "perkos_a2a_send",
29464
29800
  description: "Send a task to another agent via PerkOS A2A. Use this to delegate work, ask a peer to research, or route a chain/circular workflow. The message may name additional agents and a final notification target; the bridge preserves the route and prevents loops.",
@@ -29645,13 +29981,13 @@ function register(api) {
29645
29981
  required: ["convId", "walletAddress", "text"]
29646
29982
  },
29647
29983
  async execute(_id, params) {
29648
- if (!chatClient) {
29984
+ if (!chatEnabled) {
29649
29985
  return {
29650
29986
  content: [{ type: "text", text: "PerkOS-Chat client is not enabled in this agent's config." }]
29651
29987
  };
29652
29988
  }
29653
29989
  try {
29654
- const result = await chatClient.sendReply({
29990
+ const result = await supervisorClient.sendChatReply({
29655
29991
  convId: params.convId,
29656
29992
  walletAddress: params.walletAddress,
29657
29993
  text: params.text,
@@ -29692,14 +30028,13 @@ function register(api) {
29692
30028
  required: ["convId"]
29693
30029
  },
29694
30030
  async execute(_id, params) {
29695
- if (!chatClient) {
30031
+ if (!chatStore) {
29696
30032
  return {
29697
30033
  content: [{ type: "text", text: "PerkOS-Chat client is not enabled in this agent's config." }]
29698
30034
  };
29699
30035
  }
29700
30036
  try {
29701
- const store = chatClient.getStore();
29702
- const page = await store.readPage(params.convId, {
30037
+ const page = await chatStore.readPage(params.convId, {
29703
30038
  before: params.before ?? null,
29704
30039
  limit: params.limit ?? 50
29705
30040
  });
@@ -29728,9 +30063,9 @@ function register(api) {
29728
30063
  hasSystemEventInjection: !!enqueueSystemEvent,
29729
30064
  protocol: "a2a",
29730
30065
  version: "0.8.4",
29731
- chat: chatClient ? {
30066
+ chat: chatEnabled ? {
29732
30067
  enabled: true,
29733
- connected: chatClient.isConnected(),
30068
+ connected: supervisorClient.isChatConnected(),
29734
30069
  url: pluginConfig.chat?.url || "wss://chat.perkos.xyz/chat"
29735
30070
  } : { enabled: false, connected: false, url: null }
29736
30071
  });
@@ -29748,8 +30083,8 @@ function register(api) {
29748
30083
  console.log(`Auth required: ${pluginConfig.auth?.requireApiKey || false}`);
29749
30084
  console.log(`Peers: ${Object.keys(pluginConfig.peers).join(", ") || "(none)"}`);
29750
30085
  console.log(`System event injection: ${!!enqueueSystemEvent}`);
29751
- if (chatClient) {
29752
- console.log(`Chat: enabled (connected=${chatClient.isConnected()}, url=${pluginConfig.chat?.url || "wss://chat.perkos.xyz/chat"})`);
30086
+ if (chatEnabled) {
30087
+ console.log(`Chat: enabled (connected=${supervisorClient.isChatConnected()}, url=${pluginConfig.chat?.url || "wss://chat.perkos.xyz/chat"})`);
29753
30088
  } else {
29754
30089
  console.log("Chat: disabled");
29755
30090
  }
@@ -29770,26 +30105,26 @@ function register(api) {
29770
30105
  console.log("[perkos-a2a] Detecting environment...\n");
29771
30106
  const isMac = process.platform === "darwin";
29772
30107
  console.log(`Platform: ${process.platform}${isMac ? " (macOS)" : ""}`);
29773
- const net = await detectNetworking();
29774
- console.log(`Public IP: ${net.publicIp || "unknown"}`);
29775
- console.log(`Local IPs: ${net.localIps.join(", ") || "none"}`);
29776
- console.log(`Behind NAT: ${net.isBehindNat ? "yes" : "no"}`);
29777
- console.log(`Tailscale: ${net.hasTailscale ? "installed" : "not found"}${net.tailscaleIp ? ` (${net.tailscaleIp})` : ""}`);
30108
+ const net2 = await detectNetworking();
30109
+ console.log(`Public IP: ${net2.publicIp || "unknown"}`);
30110
+ console.log(`Local IPs: ${net2.localIps.join(", ") || "none"}`);
30111
+ console.log(`Behind NAT: ${net2.isBehindNat ? "yes" : "no"}`);
30112
+ console.log(`Tailscale: ${net2.hasTailscale ? "installed" : "not found"}${net2.tailscaleIp ? ` (${net2.tailscaleIp})` : ""}`);
29778
30113
  let portAvailable = true;
29779
30114
  try {
29780
30115
  const netMod = await import("net");
29781
- await new Promise((resolve3, reject) => {
30116
+ await new Promise((resolve4, reject) => {
29782
30117
  const srv = netMod.createServer();
29783
30118
  srv.once("error", (err) => {
29784
30119
  if (err.code === "EADDRINUSE") {
29785
30120
  portAvailable = false;
29786
- resolve3();
30121
+ resolve4();
29787
30122
  } else {
29788
30123
  reject(err);
29789
30124
  }
29790
30125
  });
29791
30126
  srv.listen(pluginConfig.port, () => {
29792
- srv.close(() => resolve3());
30127
+ srv.close(() => resolve4());
29793
30128
  });
29794
30129
  });
29795
30130
  } catch {
@@ -29809,14 +30144,14 @@ Relay: ${pluginConfig.relay?.enabled ? "enabled" : "disabled"}`);
29809
30144
  console.log(` Local loopback URL for this agent: http://127.0.0.1:${pluginConfig.port}/a2a/jsonrpc`);
29810
30145
  console.log(" Configure same-host peers as http://127.0.0.1:<peer-port>, not by shared public IP.");
29811
30146
  console.log("");
29812
- if (!net.isBehindNat) {
30147
+ if (!net2.isBehindNat) {
29813
30148
  console.log("You have a public IP. Use it only for remote peers that cannot reach a local/tunnel URL.");
29814
- console.log(` Public A2A URL candidate: http://${net.publicIp}:${pluginConfig.port}/a2a/jsonrpc`);
30149
+ console.log(` Public A2A URL candidate: http://${net2.publicIp}:${pluginConfig.port}/a2a/jsonrpc`);
29815
30150
  } else if (pluginConfig.relay?.enabled) {
29816
30151
  console.log("You are behind NAT but relay is configured. Bidirectional communication via relay.");
29817
- } else if (net.hasTailscale && net.tailscaleIp) {
30152
+ } else if (net2.hasTailscale && net2.tailscaleIp) {
29818
30153
  console.log("Use your Tailscale IP for peers.");
29819
- console.log(` Your A2A URL: http://${net.tailscaleIp}:${pluginConfig.port}/a2a/jsonrpc`);
30154
+ console.log(` Your A2A URL: http://${net2.tailscaleIp}:${pluginConfig.port}/a2a/jsonrpc`);
29820
30155
  } else {
29821
30156
  console.log("You are behind NAT. Options:");
29822
30157
  console.log(" 1) Configure a relay hub (recommended) - set relay.url and relay.enabled in config");