@perkos/perkos-a2a 0.12.30 → 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/README.md CHANGED
@@ -170,7 +170,7 @@ openclaw gateway restart
170
170
 
171
171
  #### Official idempotent connection
172
172
 
173
- Version 0.12.30+ ships the machine-readable `perkos-a2a connect` CLI. The
173
+ Version 0.12.31+ ships the machine-readable `perkos-a2a connect` CLI. The
174
174
  official PerkOS invitation supplies the artifact URL, SHA-256, agent name, and
175
175
  agent id; it invokes the exact npm version with `npx`. The same command performs
176
176
  a first install or repairs the matching existing identity, even when an invalid
@@ -203,7 +203,7 @@ values are never printed.
203
203
 
204
204
  ```bash
205
205
  A2A_RELAY_API_KEY='<credential-from-invitation>' \
206
- npx --yes @perkos/perkos-a2a@0.12.30 connect \
206
+ npx --yes @perkos/perkos-a2a@0.12.31 connect \
207
207
  --artifact ./perkos-a2a.tgz \
208
208
  --sha256 '<sha256-from-the-official-invitation>' \
209
209
  --agent-name '<agent-name>' \
package/dist/index.d.ts CHANGED
@@ -51,5 +51,7 @@ export declare function runOpenClawChatTurn(api: any, config: A2APluginConfig, s
51
51
  }): Promise<string | null>;
52
52
  /** Build a bounded, agent-local transcript so every runtime turn has continuity. */
53
53
  export declare function formatRecentChatContext(messages: ChatMessage[], currentMessageId: string): string;
54
+ /** The identity the relay treats as exclusive; names are display metadata. */
55
+ export declare function a2aLifecycleKey(config: A2APluginConfig): string;
54
56
  export default function register(api: any): void;
55
57
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAYH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,KAAK,EAGV,WAAW,EAEZ,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,cAAc,YAAY,CAAC;AA0C3B,kEAAkE;AAClE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAItF;AAED,KAAK,oBAAoB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AASpD;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,OAAO,EACd,oBAAoB,EAAE,MAAM,GAC3B,OAAO,CA2BT;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,oBAAoB,EAC3B,eAAe,CAAC,EAAE,MAAM,GACvB,oBAAoB,CA+BtB;AAiED,KAAK,qBAAqB,GAAG;IAC3B,QAAQ,CAAC,EAAE,KAAK,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,YAAY,CAAC,EAAE,OAAO,CAAC;KACxB,CAAC,CAAC;IACH,IAAI,CAAC,EAAE;QACL,yBAAyB,CAAC,EAAE,MAAM,CAAC;QACnC,qBAAqB,CAAC,EAAE,MAAM,CAAC;KAChC,CAAC;CACH,CAAC;AAEF,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,GAAG,IAAI,CAYrF;AAQD,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,eAAe,EACvB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAA;CAAE,GAClF,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAuFxB;AAYD,oFAAoF;AACpF,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,WAAW,EAAE,EACvB,gBAAgB,EAAE,MAAM,GACvB,MAAM,CAWR;AA4CD,MAAM,CAAC,OAAO,UAAU,QAAQ,CAAC,GAAG,EAAE,GAAG,QAq1BxC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAYH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,KAAK,EAGV,WAAW,EAEZ,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,cAAc,YAAY,CAAC;AA0C3B,kEAAkE;AAClE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAItF;AAED,KAAK,oBAAoB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AASpD;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,OAAO,EACd,oBAAoB,EAAE,MAAM,GAC3B,OAAO,CA2BT;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,oBAAoB,EAC3B,eAAe,CAAC,EAAE,MAAM,GACvB,oBAAoB,CA+BtB;AAiED,KAAK,qBAAqB,GAAG;IAC3B,QAAQ,CAAC,EAAE,KAAK,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,YAAY,CAAC,EAAE,OAAO,CAAC;KACxB,CAAC,CAAC;IACH,IAAI,CAAC,EAAE;QACL,yBAAyB,CAAC,EAAE,MAAM,CAAC;QACnC,qBAAqB,CAAC,EAAE,MAAM,CAAC;KAChC,CAAC;CACH,CAAC;AAEF,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,GAAG,IAAI,CAYrF;AAQD,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,eAAe,EACvB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAA;CAAE,GAClF,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAuFxB;AAYD,oFAAoF;AACpF,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,WAAW,EAAE,EACvB,gBAAgB,EAAE,MAAM,GACvB,MAAM,CAWR;AAoDD,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAe/D;AAED,MAAM,CAAC,OAAO,UAAU,QAAQ,CAAC,GAAG,EAAE,GAAG,QAi2BxC"}
package/dist/index.js CHANGED
@@ -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
  }
@@ -26464,6 +26502,10 @@ var RelayClient = class {
26464
26502
  } else {
26465
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);
@@ -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;
@@ -28871,6 +29077,28 @@ async function deliverToHermes(config, message, logger) {
28871
29077
  }
28872
29078
  logger.info(`[perkos-a2a] Delivered inbound A2A task to Hermes API session ${sessionKey} via ${url}`);
28873
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
+ }
28874
29102
  function register(api) {
28875
29103
  const pluginConfig = api.config?.plugins?.entries?.["perkos-a2a"]?.config || {
28876
29104
  agentName: "agent",
@@ -28887,6 +29115,8 @@ function register(api) {
28887
29115
  logger
28888
29116
  });
28889
29117
  const server = new A2AServer(pluginConfig, logger);
29118
+ const lifecycleKey = a2aLifecycleKey(pluginConfig);
29119
+ const lifecycleInstanceId = randomUUID6();
28890
29120
  const pendingTasks = [];
28891
29121
  const enqueueSystemEvent = api.runtime?.system?.enqueueSystemEvent;
28892
29122
  const requestHeartbeatNow = api.runtime?.system?.requestHeartbeatNow;
@@ -29058,12 +29288,22 @@ function register(api) {
29058
29288
  });
29059
29289
  api.registerService({
29060
29290
  id: "perkos-a2a",
29061
- start: () => {
29062
- 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();
29063
29300
  logger.info(`[perkos-a2a] A2A server started for ${pluginConfig.agentName}`);
29064
29301
  },
29065
- stop: () => {
29302
+ stop: async () => {
29066
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);
29067
29307
  }
29068
29308
  });
29069
29309
  const chatApiKey = pluginConfig.chat?.apiKey || pluginConfig.relay?.apiKey;
@@ -29600,6 +29840,7 @@ export {
29600
29840
  ChatStore,
29601
29841
  RelayClient,
29602
29842
  RelayHub,
29843
+ a2aLifecycleKey,
29603
29844
  alignOpenClawChatSessionModel,
29604
29845
  register as default,
29605
29846
  detectNetworking,