@perkos/perkos-a2a 0.12.29 → 0.12.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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(resolve, reject) {
4947
+ return new Promise(function executor(resolve3, reject) {
4948
4948
  readStream(stream, encoding, length, limit, function onRead(err, buf) {
4949
4949
  if (err) return reject(err);
4950
- resolve(buf);
4950
+ resolve3(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 dirname = path.dirname;
18432
+ var dirname2 = path.dirname;
18433
18433
  var basename = path.basename;
18434
18434
  var extname = path.extname;
18435
- var join2 = path.join;
18436
- var resolve = path.resolve;
18435
+ var join3 = path.join;
18436
+ var resolve3 = 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 = resolve(root, name);
18471
- var dir = dirname(loc);
18470
+ var loc = resolve3(root, name);
18471
+ var dir = dirname2(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 resolve2(dir, file) {
18481
+ View.prototype.resolve = function resolve4(dir, file) {
18482
18482
  var ext = this.ext;
18483
- var path2 = join2(dir, file);
18483
+ var path2 = join3(dir, file);
18484
18484
  var stat = tryStat(path2);
18485
18485
  if (stat && stat.isFile()) {
18486
18486
  return path2;
18487
18487
  }
18488
- path2 = join2(dir, basename(file, ext), "index" + ext);
18488
+ path2 = join3(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 join2 = path.join;
19121
+ var join3 = path.join;
19122
19122
  var normalize = path.normalize;
19123
- var resolve = path.resolve;
19123
+ var resolve3 = 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 ? resolve(opts.root) : null;
19160
+ this._root = opts.root ? resolve3(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 = resolve(String(path2));
19184
+ this._root = resolve3(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(join2(root, path2));
19340
+ path2 = normalize(join3(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 = resolve(path2);
19348
+ path2 = resolve3(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 = join2(path2, self._index[i]);
19475
+ var p = join3(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 resolve = __require("path").resolve;
20628
+ var resolve3 = __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", resolve("views"));
20667
+ this.set("views", resolve3("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 resolve = path.resolve;
21912
+ var resolve3 = 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 ? resolve(path2) : path2;
22171
+ var fullPath = !opts.root ? resolve3(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 resolve = __require("path").resolve;
22437
+ var resolve3 = __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 = resolve(root);
22457
+ opts.root = resolve3(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") {
@@ -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((resolve3) => {
26295
+ let settled = false;
26296
+ const finish = (closeTimedOut) => {
26297
+ if (settled) return;
26298
+ settled = true;
26299
+ clearTimeout(timer);
26300
+ resolve3({ 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((resolve, reject) => {
26491
+ return new Promise((resolve3, 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
- resolve(response);
26503
+ resolve3(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((resolve3) => {
26607
+ let settled = false;
26608
+ const done = () => {
26609
+ if (settled) return;
26610
+ settled = true;
26611
+ clearTimeout(timer);
26612
+ resolve3();
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
  }
@@ -26938,6 +27013,12 @@ var A2AServer = class {
26938
27013
  taskResultHandler = null;
26939
27014
  taskFailureHandler = null;
26940
27015
  lastRuntimeDelivery = null;
27016
+ httpServer = null;
27017
+ lifecycleState = "new";
27018
+ generation = 0;
27019
+ startPromise = null;
27020
+ stopPromise = null;
27021
+ operations = /* @__PURE__ */ new Set();
26941
27022
  constructor(config, logger) {
26942
27023
  this.config = config;
26943
27024
  this.logger = logger || { info: console.log, error: console.error };
@@ -27068,6 +27149,9 @@ var A2AServer = class {
27068
27149
  });
27069
27150
  }
27070
27151
  async handleSendMessage(params, rpcId) {
27152
+ if (this.lifecycleState === "stopping" || this.lifecycleState === "stopped") {
27153
+ return this.error(rpcId, 503, "SERVER_RELOADING");
27154
+ }
27071
27155
  const message = params.message;
27072
27156
  const taskId = randomUUID4();
27073
27157
  const contextId = message?.contextId || randomUUID4();
@@ -27088,10 +27172,11 @@ var A2AServer = class {
27088
27172
  this.logger.info(
27089
27173
  `[perkos-a2a] Task ${taskId} received from ${task.metadata?.fromAgent}`
27090
27174
  );
27091
- void this.processTask(task);
27175
+ const generation = this.generation;
27176
+ this.trackOperation(this.processTask(task, generation));
27092
27177
  return this.success(rpcId, task);
27093
27178
  }
27094
- async processTask(task) {
27179
+ async processTask(task, generation) {
27095
27180
  task.status = { state: "working", timestamp: (/* @__PURE__ */ new Date()).toISOString() };
27096
27181
  this.tasks.set(task.id, structuredClone(task));
27097
27182
  const textParts = task.messages.flatMap((m) => m.parts || []).filter((p) => p.kind === "text").map((p) => p.text).join("\n");
@@ -27135,9 +27220,11 @@ var A2AServer = class {
27135
27220
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
27136
27221
  };
27137
27222
  }
27223
+ if (!this.isGenerationActive(generation)) return;
27138
27224
  this.tasks.set(task.id, structuredClone(task));
27139
27225
  this.logger.info(`[perkos-a2a] Task ${task.id} completed`);
27140
27226
  } catch (err) {
27227
+ if (!this.isGenerationActive(generation)) return;
27141
27228
  const msg = err instanceof Error ? err.message : String(err);
27142
27229
  if (this.taskFailureHandler) {
27143
27230
  await this.taskFailureHandler(task, msg);
@@ -27237,7 +27324,7 @@ var A2AServer = class {
27237
27324
  async waitForRemoteTaskTerminal(targetUrl, headers, taskId, timeoutMs = 45e3, pollMs = 1e3) {
27238
27325
  const deadline = Date.now() + timeoutMs;
27239
27326
  while (Date.now() < deadline) {
27240
- await new Promise((resolve) => setTimeout(resolve, pollMs));
27327
+ await new Promise((resolve3) => setTimeout(resolve3, pollMs));
27241
27328
  const response = await fetch(`${targetUrl}/a2a/jsonrpc`, {
27242
27329
  method: "POST",
27243
27330
  headers,
@@ -27261,7 +27348,7 @@ var A2AServer = class {
27261
27348
  if (task && isTerminalTaskState(task.status?.state)) {
27262
27349
  return this.success(randomUUID4(), structuredClone(task));
27263
27350
  }
27264
- await new Promise((resolve) => setTimeout(resolve, pollMs));
27351
+ await new Promise((resolve3) => setTimeout(resolve3, pollMs));
27265
27352
  }
27266
27353
  return null;
27267
27354
  }
@@ -27324,10 +27411,17 @@ var A2AServer = class {
27324
27411
  }
27325
27412
  /** Handle an inbound task received via the relay */
27326
27413
  handleRelayTask(msg) {
27414
+ if (this.lifecycleState !== "running") {
27415
+ this.relayClient?.sendTaskResponse(
27416
+ msg,
27417
+ this.error(String(msg.payload?.id ?? msg.id), 503, "SERVER_RELOADING")
27418
+ );
27419
+ return;
27420
+ }
27327
27421
  const payload = msg.payload;
27328
27422
  const params = payload.params || payload;
27329
27423
  const rpcId = payload.id || msg.id;
27330
- this.handleSendMessage(params, rpcId).then(async (response) => {
27424
+ const operation = this.handleSendMessage(params, rpcId).then(async (response) => {
27331
27425
  const task = response.result;
27332
27426
  const acceptQueued = task?.messages?.[0]?.metadata?.acceptQueued === true;
27333
27427
  if (task?.id && !isTerminalTaskState(task.status?.state)) {
@@ -27358,6 +27452,7 @@ var A2AServer = class {
27358
27452
  this.logger.error(`[perkos-a2a] Failed to process relay task: ${errMsg2}`);
27359
27453
  this.relayClient?.sendTaskResponse(msg, this.error(rpcId, -32603, errMsg2));
27360
27454
  });
27455
+ this.trackOperation(operation);
27361
27456
  }
27362
27457
  startRelayClient() {
27363
27458
  const relay = this.config.relay;
@@ -27389,13 +27484,30 @@ var A2AServer = class {
27389
27484
  this.relayHub.start();
27390
27485
  }
27391
27486
  async start() {
27487
+ if (this.lifecycleState === "running") return;
27488
+ if (this.lifecycleState === "starting" && this.startPromise) return this.startPromise;
27489
+ if (this.lifecycleState === "stopping" && this.stopPromise) await this.stopPromise;
27490
+ const generation = ++this.generation;
27491
+ this.lifecycleState = "starting";
27492
+ const startPromise = this.startGeneration(generation);
27493
+ this.startPromise = startPromise;
27494
+ try {
27495
+ await startPromise;
27496
+ if (this.isGenerationActive(generation, "starting")) this.lifecycleState = "running";
27497
+ } finally {
27498
+ if (this.startPromise === startPromise) this.startPromise = null;
27499
+ }
27500
+ }
27501
+ async startGeneration(generation) {
27392
27502
  const mode = this.config.mode || "auto";
27393
27503
  if (mode === "relay") {
27504
+ if (!this.isGenerationActive(generation, "starting")) return;
27394
27505
  this.clientOnly = true;
27395
27506
  this.startRelayHub();
27396
27507
  return;
27397
27508
  }
27398
27509
  if (mode === "client-only") {
27510
+ if (!this.isGenerationActive(generation, "starting")) return;
27399
27511
  this.clientOnly = true;
27400
27512
  this.startRelayClient();
27401
27513
  if (!this.config.relay?.enabled) {
@@ -27406,12 +27518,14 @@ var A2AServer = class {
27406
27518
  return;
27407
27519
  }
27408
27520
  if (mode === "full") {
27409
- this.tryListen(this.config.port);
27521
+ await this.tryListen(this.config.port, 1, generation);
27522
+ if (!this.isGenerationActive(generation, "starting")) return;
27410
27523
  this.startRelayClient();
27411
27524
  return;
27412
27525
  }
27413
27526
  try {
27414
27527
  const net = await detectNetworking();
27528
+ if (!this.isGenerationActive(generation, "starting")) return;
27415
27529
  if (net.isBehindNat && !net.hasTailscale) {
27416
27530
  if (this.config.relay?.enabled) {
27417
27531
  this.logger.info(
@@ -27434,37 +27548,129 @@ var A2AServer = class {
27434
27548
  }
27435
27549
  } catch {
27436
27550
  }
27437
- this.tryListen(this.config.port);
27551
+ await this.tryListen(this.config.port, 1, generation);
27552
+ if (!this.isGenerationActive(generation, "starting")) return;
27438
27553
  this.startRelayClient();
27439
27554
  }
27440
- tryListen(port, attempt = 1) {
27555
+ stop(options = {}) {
27556
+ if (this.lifecycleState === "stopping" && this.stopPromise) return this.stopPromise;
27557
+ if (this.lifecycleState === "stopped" || this.lifecycleState === "new") {
27558
+ this.lifecycleState = "stopped";
27559
+ this.generation += 1;
27560
+ return Promise.resolve();
27561
+ }
27562
+ this.lifecycleState = "stopping";
27563
+ this.generation += 1;
27564
+ const priorStart = this.startPromise;
27565
+ let stopPromise;
27566
+ stopPromise = (async () => {
27567
+ try {
27568
+ const timeoutMs = options.timeoutMs ?? 5e3;
27569
+ const drainMs = Math.min(options.drainMs ?? 3e3, timeoutMs);
27570
+ await this.teardownResources(timeoutMs);
27571
+ await priorStart?.catch(() => void 0);
27572
+ await this.teardownResources(timeoutMs);
27573
+ await this.drainOperations(drainMs);
27574
+ for (const task of this.tasks.values()) {
27575
+ if (!isTerminalTaskState(task.status.state)) {
27576
+ task.status = {
27577
+ state: "failed",
27578
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
27579
+ message: { role: "agent", parts: [{ kind: "text", text: "interrupted_by_reload" }] }
27580
+ };
27581
+ }
27582
+ }
27583
+ this.lifecycleState = "stopped";
27584
+ this.logger.info(`[perkos-a2a] A2A server stopped${options.reason ? ` (${options.reason})` : ""}`);
27585
+ } finally {
27586
+ if (this.stopPromise === stopPromise) this.stopPromise = null;
27587
+ }
27588
+ })();
27589
+ this.stopPromise = stopPromise;
27590
+ return stopPromise;
27591
+ }
27592
+ tryListen(port, attempt, generation) {
27441
27593
  const maxAttempts = 3;
27442
- try {
27594
+ return new Promise((resolveStart) => {
27595
+ if (!this.isGenerationActive(generation, "starting")) {
27596
+ resolveStart();
27597
+ return;
27598
+ }
27443
27599
  const bindHost = this.config.bindHost || "0.0.0.0";
27444
27600
  const srv = this.app.listen(port, bindHost, () => {
27601
+ if (!this.isGenerationActive(generation, "starting")) {
27602
+ srv.close(() => resolveStart());
27603
+ return;
27604
+ }
27605
+ this.httpServer = srv;
27445
27606
  this.logger.info(
27446
27607
  `[perkos-a2a] ${this.config.agentName} server on ${bindHost}:${port}`
27447
27608
  );
27448
27609
  this.agentCard.url = `${(this.config.publicUrl || `http://localhost:${port}`).replace(/\/+$/, "")}/a2a/jsonrpc`;
27610
+ resolveStart();
27449
27611
  });
27450
27612
  srv.on("error", (err) => {
27451
27613
  if (err.code === "EADDRINUSE" && attempt < maxAttempts) {
27452
27614
  const nextPort = port + 1;
27453
27615
  this.logger.info(`[perkos-a2a] Port ${port} in use, trying ${nextPort}`);
27454
- this.tryListen(nextPort, attempt + 1);
27616
+ void this.tryListen(nextPort, attempt + 1, generation).then(resolveStart);
27455
27617
  } else if (err.code === "EADDRINUSE") {
27456
27618
  this.logger.info(
27457
27619
  `[perkos-a2a] Ports ${this.config.port}-${port} all in use. Falling back to client-only mode.`
27458
27620
  );
27459
27621
  this.clientOnly = true;
27622
+ resolveStart();
27460
27623
  } else {
27461
27624
  this.logger.error(`[perkos-a2a] Server error: ${err.message}`);
27625
+ resolveStart();
27462
27626
  }
27463
27627
  });
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
- }
27628
+ });
27629
+ }
27630
+ isGenerationActive(generation, expectedState) {
27631
+ return generation === this.generation && (expectedState ? this.lifecycleState === expectedState : this.lifecycleState !== "stopping" && this.lifecycleState !== "stopped");
27632
+ }
27633
+ trackOperation(operation) {
27634
+ this.operations.add(operation);
27635
+ void operation.finally(() => this.operations.delete(operation)).catch(() => void 0);
27636
+ }
27637
+ async drainOperations(timeoutMs) {
27638
+ if (this.operations.size === 0 || timeoutMs <= 0) return;
27639
+ await Promise.race([
27640
+ Promise.allSettled([...this.operations]),
27641
+ new Promise((resolveDrain) => {
27642
+ const timer = setTimeout(resolveDrain, timeoutMs);
27643
+ timer.unref?.();
27644
+ })
27645
+ ]);
27646
+ }
27647
+ async teardownResources(timeoutMs) {
27648
+ const deadline = Date.now() + timeoutMs;
27649
+ const remaining = () => Math.max(1, deadline - Date.now());
27650
+ const relayClient = this.relayClient;
27651
+ this.relayClient = null;
27652
+ await relayClient?.stopAndWait(remaining());
27653
+ const relayHub = this.relayHub;
27654
+ this.relayHub = null;
27655
+ await relayHub?.stop(remaining());
27656
+ const httpServer = this.httpServer;
27657
+ this.httpServer = null;
27658
+ if (!httpServer) return;
27659
+ await new Promise((resolveClose) => {
27660
+ let settled = false;
27661
+ const done = () => {
27662
+ if (settled) return;
27663
+ settled = true;
27664
+ clearTimeout(timer);
27665
+ resolveClose();
27666
+ };
27667
+ const timer = setTimeout(() => {
27668
+ httpServer.closeAllConnections?.();
27669
+ done();
27670
+ }, remaining());
27671
+ timer.unref?.();
27672
+ httpServer.close(() => done());
27673
+ });
27468
27674
  }
27469
27675
  getExpressApp() {
27470
27676
  return this.app;
@@ -27473,6 +27679,9 @@ var A2AServer = class {
27473
27679
 
27474
27680
  // src/index.ts
27475
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";
27476
27685
 
27477
27686
  // src/agentic-actions.ts
27478
27687
  var FALLBACK_AGENT_ALIASES = [
@@ -28017,7 +28226,7 @@ var ChatStore = class {
28017
28226
  let firstMessageAt = null;
28018
28227
  let lastMessageAt = null;
28019
28228
  let leftover = "";
28020
- return new Promise((resolve, reject) => {
28229
+ return new Promise((resolve3, reject) => {
28021
28230
  stream.on("data", (chunk) => {
28022
28231
  const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
28023
28232
  hash.update(text);
@@ -28057,7 +28266,7 @@ var ChatStore = class {
28057
28266
  hash.update(metaRaw);
28058
28267
  } catch {
28059
28268
  }
28060
- resolve({
28269
+ resolve3({
28061
28270
  transcriptHash: hash.digest("hex"),
28062
28271
  hashAlgo: "sha256",
28063
28272
  messageCount,
@@ -28526,12 +28735,97 @@ function startPlatformHeartbeat(opts) {
28526
28735
  };
28527
28736
  }
28528
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
+
28529
28815
  // src/types.ts
28530
28816
  function isWebhookEvent(msg) {
28531
28817
  return msg.type === "webhook_event";
28532
28818
  }
28533
28819
 
28534
28820
  // src/index.ts
28821
+ function loadedPluginVersion() {
28822
+ try {
28823
+ const manifest = JSON.parse(readFileSync3(new URL("../openclaw.plugin.json", import.meta.url), "utf8"));
28824
+ return typeof manifest.version === "string" ? manifest.version : "unknown";
28825
+ } catch {
28826
+ return "unknown";
28827
+ }
28828
+ }
28535
28829
  function wakeGatewayAgent(requestHeartbeatNow, reason, logger, sessionKey = "agent:main") {
28536
28830
  if (typeof requestHeartbeatNow === "function") {
28537
28831
  try {
@@ -28783,6 +29077,28 @@ async function deliverToHermes(config, message, logger) {
28783
29077
  }
28784
29078
  logger.info(`[perkos-a2a] Delivered inbound A2A task to Hermes API session ${sessionKey} via ${url}`);
28785
29079
  }
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
+ function a2aLifecycleKey(config) {
29086
+ const rawRelayUrl = (config.relay?.url || "no-relay").trim();
29087
+ let relayUrl = rawRelayUrl.replace(/[?#].*$/u, "").replace(/\/+$/u, "").toLowerCase();
29088
+ try {
29089
+ const parsed = new URL(rawRelayUrl);
29090
+ parsed.username = "";
29091
+ parsed.password = "";
29092
+ parsed.search = "";
29093
+ parsed.hash = "";
29094
+ relayUrl = parsed.toString().replace(/\/+$/u, "").toLowerCase();
29095
+ } catch {
29096
+ }
29097
+ const heartbeatUrl = config.platform?.heartbeatUrl || "";
29098
+ const idFromHeartbeat = heartbeatUrl.match(/\/agents\/([^/]+)\/heartbeat(?:\?|$)/u)?.[1];
29099
+ const agentId = config.platform?.agentId || idFromHeartbeat || config.agentName.trim().toLowerCase();
29100
+ return `${relayUrl}|${agentId}`;
29101
+ }
28786
29102
  function register(api) {
28787
29103
  const pluginConfig = api.config?.plugins?.entries?.["perkos-a2a"]?.config || {
28788
29104
  agentName: "agent",
@@ -28791,7 +29107,16 @@ function register(api) {
28791
29107
  peers: {}
28792
29108
  };
28793
29109
  const logger = api.logger || console;
29110
+ const pluginVersion = loadedPluginVersion();
29111
+ recordRuntimeLoadEvidence({
29112
+ config: pluginConfig,
29113
+ version: pluginVersion,
29114
+ pluginRoot: resolve2(dirname(fileURLToPath(import.meta.url)), ".."),
29115
+ logger
29116
+ });
28794
29117
  const server = new A2AServer(pluginConfig, logger);
29118
+ const lifecycleKey = a2aLifecycleKey(pluginConfig);
29119
+ const lifecycleInstanceId = randomUUID6();
28795
29120
  const pendingTasks = [];
28796
29121
  const enqueueSystemEvent = api.runtime?.system?.enqueueSystemEvent;
28797
29122
  const requestHeartbeatNow = api.runtime?.system?.requestHeartbeatNow;
@@ -28963,12 +29288,22 @@ function register(api) {
28963
29288
  });
28964
29289
  api.registerService({
28965
29290
  id: "perkos-a2a",
28966
- start: () => {
28967
- server.start();
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();
28968
29300
  logger.info(`[perkos-a2a] A2A server started for ${pluginConfig.agentName}`);
28969
29301
  },
28970
- stop: () => {
29302
+ stop: async () => {
28971
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);
28972
29307
  }
28973
29308
  });
28974
29309
  const chatApiKey = pluginConfig.chat?.apiKey || pluginConfig.relay?.apiKey;
@@ -29114,7 +29449,7 @@ function register(api) {
29114
29449
  url: pluginConfig.platform?.heartbeatUrl,
29115
29450
  relayKey: pluginConfig.relay?.apiKey,
29116
29451
  runtimeKind: "openclaw",
29117
- version: "0.12.26",
29452
+ version: pluginVersion,
29118
29453
  intervalMs: pluginConfig.platform?.heartbeatIntervalMs
29119
29454
  });
29120
29455
  },
@@ -29443,18 +29778,18 @@ function register(api) {
29443
29778
  let portAvailable = true;
29444
29779
  try {
29445
29780
  const netMod = await import("net");
29446
- await new Promise((resolve, reject) => {
29781
+ await new Promise((resolve3, reject) => {
29447
29782
  const srv = netMod.createServer();
29448
29783
  srv.once("error", (err) => {
29449
29784
  if (err.code === "EADDRINUSE") {
29450
29785
  portAvailable = false;
29451
- resolve();
29786
+ resolve3();
29452
29787
  } else {
29453
29788
  reject(err);
29454
29789
  }
29455
29790
  });
29456
29791
  srv.listen(pluginConfig.port, () => {
29457
- srv.close(() => resolve());
29792
+ srv.close(() => resolve3());
29458
29793
  });
29459
29794
  });
29460
29795
  } catch {
@@ -29505,6 +29840,7 @@ export {
29505
29840
  ChatStore,
29506
29841
  RelayClient,
29507
29842
  RelayHub,
29843
+ a2aLifecycleKey,
29508
29844
  alignOpenClawChatSessionModel,
29509
29845
  register as default,
29510
29846
  detectNetworking,