@perkos/perkos-a2a 0.12.66 → 0.12.68

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 (73) hide show
  1. package/dist/a2a-maintenance.d.ts +3 -5
  2. package/dist/a2a-maintenance.d.ts.map +1 -1
  3. package/dist/a2a-maintenance.js +37 -23
  4. package/dist/a2a-maintenance.js.map +1 -1
  5. package/dist/bridge-agent.js +49 -1
  6. package/dist/bridge-agent.js.map +1 -1
  7. package/dist/bridge-chat-reply.d.ts +4 -1
  8. package/dist/bridge-chat-reply.d.ts.map +1 -1
  9. package/dist/bridge-chat-reply.js +22 -13
  10. package/dist/bridge-chat-reply.js.map +1 -1
  11. package/dist/chat-client.d.ts +11 -4
  12. package/dist/chat-client.d.ts.map +1 -1
  13. package/dist/chat-client.js +57 -13
  14. package/dist/chat-client.js.map +1 -1
  15. package/dist/chat-outbox.d.ts +26 -0
  16. package/dist/chat-outbox.d.ts.map +1 -0
  17. package/dist/chat-outbox.js +73 -0
  18. package/dist/chat-outbox.js.map +1 -0
  19. package/dist/chat-runtime-http.d.ts +9 -0
  20. package/dist/chat-runtime-http.d.ts.map +1 -0
  21. package/dist/chat-runtime-http.js +86 -0
  22. package/dist/chat-runtime-http.js.map +1 -0
  23. package/dist/chat-store.d.ts.map +1 -1
  24. package/dist/chat-store.js +10 -2
  25. package/dist/chat-store.js.map +1 -1
  26. package/dist/chat-turns.d.ts +44 -0
  27. package/dist/chat-turns.d.ts.map +1 -0
  28. package/dist/chat-turns.js +146 -0
  29. package/dist/chat-turns.js.map +1 -0
  30. package/dist/chat-types.d.ts +4 -0
  31. package/dist/chat-types.d.ts.map +1 -1
  32. package/dist/hermes-connect.d.ts +2 -0
  33. package/dist/hermes-connect.d.ts.map +1 -1
  34. package/dist/hermes-connect.js +11 -0
  35. package/dist/hermes-connect.js.map +1 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +725 -148
  38. package/dist/index.js.map +4 -4
  39. package/dist/maintenance-result.d.ts +19 -0
  40. package/dist/maintenance-result.d.ts.map +1 -0
  41. package/dist/maintenance-result.js +38 -0
  42. package/dist/maintenance-result.js.map +1 -0
  43. package/dist/maintenance-runner.d.ts +15 -0
  44. package/dist/maintenance-runner.d.ts.map +1 -1
  45. package/dist/maintenance-runner.js +87 -62
  46. package/dist/maintenance-runner.js.map +1 -1
  47. package/dist/maintenance-runner.test.d.ts +2 -0
  48. package/dist/maintenance-runner.test.d.ts.map +1 -0
  49. package/dist/maintenance-runner.test.js +153 -0
  50. package/dist/maintenance-runner.test.js.map +1 -0
  51. package/dist/openclaw-supervisor-client.d.ts +8 -0
  52. package/dist/openclaw-supervisor-client.d.ts.map +1 -1
  53. package/dist/openclaw-supervisor-client.js +3 -0
  54. package/dist/openclaw-supervisor-client.js.map +1 -1
  55. package/dist/openclaw-supervisor.d.ts.map +1 -1
  56. package/dist/openclaw-supervisor.js +9 -0
  57. package/dist/openclaw-supervisor.js.map +1 -1
  58. package/dist/private-chat-records.d.ts +12 -0
  59. package/dist/private-chat-records.d.ts.map +1 -0
  60. package/dist/private-chat-records.js +71 -0
  61. package/dist/private-chat-records.js.map +1 -0
  62. package/dist/service-environment.d.ts +3 -0
  63. package/dist/service-environment.d.ts.map +1 -0
  64. package/dist/service-environment.js +8 -0
  65. package/dist/service-environment.js.map +1 -0
  66. package/dist/supervisor-protocol.d.ts +1 -1
  67. package/dist/supervisor-protocol.d.ts.map +1 -1
  68. package/docs/chat-durable-delivery.md +55 -0
  69. package/docs/nexus-communications-server.md +1 -1
  70. package/docs/plans/2026-09-06-durable-chat-design.md +32 -0
  71. package/docs/plans/2026-09-06-managed-hermes-service-path-design.md +38 -0
  72. package/openclaw.plugin.json +1 -1
  73. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -16881,11 +16881,11 @@ var require_parse = __commonJS({
16881
16881
  segments[segments.length] = parent;
16882
16882
  }
16883
16883
  var n = key.length;
16884
- var open2 = first;
16884
+ var open3 = first;
16885
16885
  var collected = 0;
16886
- while (open2 >= 0 && collected < options.depth) {
16886
+ while (open3 >= 0 && collected < options.depth) {
16887
16887
  var level = 1;
16888
- var i = open2 + 1;
16888
+ var i = open3 + 1;
16889
16889
  var close = -1;
16890
16890
  while (i < n && close < 0) {
16891
16891
  var cu = key.charCodeAt(i);
@@ -16900,23 +16900,23 @@ var require_parse = __commonJS({
16900
16900
  i += 1;
16901
16901
  }
16902
16902
  if (close < 0) {
16903
- segments[segments.length] = "[" + key.slice(open2) + "]";
16903
+ segments[segments.length] = "[" + key.slice(open3) + "]";
16904
16904
  return segments;
16905
16905
  }
16906
- var seg = key.slice(open2, close + 1);
16906
+ var seg = key.slice(open3, close + 1);
16907
16907
  var content = seg.slice(1, -1);
16908
16908
  if (!options.plainObjects && has.call(Object.prototype, content) && !options.allowPrototypes) {
16909
16909
  return;
16910
16910
  }
16911
16911
  segments[segments.length] = seg;
16912
16912
  collected += 1;
16913
- open2 = key.indexOf("[", close + 1);
16913
+ open3 = key.indexOf("[", close + 1);
16914
16914
  }
16915
- if (open2 >= 0) {
16915
+ if (open3 >= 0) {
16916
16916
  if (options.strictDepth === true) {
16917
16917
  throw new RangeError("Input depth exceeded depth option of " + options.depth + " and strictDepth is true");
16918
16918
  }
16919
- segments[segments.length] = "[" + key.slice(open2) + "]";
16919
+ segments[segments.length] = "[" + key.slice(open3) + "]";
16920
16920
  }
16921
16921
  return segments;
16922
16922
  };
@@ -18441,10 +18441,10 @@ var require_view = __commonJS({
18441
18441
  var debug = require_src()("express:view");
18442
18442
  var path = __require("path");
18443
18443
  var fs = __require("fs");
18444
- var dirname5 = path.dirname;
18444
+ var dirname6 = path.dirname;
18445
18445
  var basename = path.basename;
18446
18446
  var extname = path.extname;
18447
- var join5 = path.join;
18447
+ var join8 = path.join;
18448
18448
  var resolve4 = path.resolve;
18449
18449
  module.exports = View;
18450
18450
  function View(name, options) {
@@ -18480,7 +18480,7 @@ var require_view = __commonJS({
18480
18480
  for (var i = 0; i < roots.length && !path2; i++) {
18481
18481
  var root = roots[i];
18482
18482
  var loc = resolve4(root, name);
18483
- var dir = dirname5(loc);
18483
+ var dir = dirname6(loc);
18484
18484
  var file = basename(loc);
18485
18485
  path2 = this.resolve(dir, file);
18486
18486
  }
@@ -18492,12 +18492,12 @@ var require_view = __commonJS({
18492
18492
  };
18493
18493
  View.prototype.resolve = function resolve5(dir, file) {
18494
18494
  var ext = this.ext;
18495
- var path2 = join5(dir, file);
18495
+ var path2 = join8(dir, file);
18496
18496
  var stat = tryStat(path2);
18497
18497
  if (stat && stat.isFile()) {
18498
18498
  return path2;
18499
18499
  }
18500
- path2 = join5(dir, basename(file, ext), "index" + ext);
18500
+ path2 = join8(dir, basename(file, ext), "index" + ext);
18501
18501
  stat = tryStat(path2);
18502
18502
  if (stat && stat.isFile()) {
18503
18503
  return path2;
@@ -19130,7 +19130,7 @@ var require_send = __commonJS({
19130
19130
  var Stream = __require("stream");
19131
19131
  var util = __require("util");
19132
19132
  var extname = path.extname;
19133
- var join5 = path.join;
19133
+ var join8 = path.join;
19134
19134
  var normalize = path.normalize;
19135
19135
  var resolve4 = path.resolve;
19136
19136
  var sep = path.sep;
@@ -19349,7 +19349,7 @@ var require_send = __commonJS({
19349
19349
  return res;
19350
19350
  }
19351
19351
  parts = path2.split(sep);
19352
- path2 = normalize(join5(root, path2));
19352
+ path2 = normalize(join8(root, path2));
19353
19353
  } else {
19354
19354
  if (UP_PATH_REGEXP.test(path2)) {
19355
19355
  debug('malicious path "%s"', path2);
@@ -19484,7 +19484,7 @@ var require_send = __commonJS({
19484
19484
  if (err) return self.onStatError(err);
19485
19485
  return self.error(404);
19486
19486
  }
19487
- var p = join5(path2, self._index[i]);
19487
+ var p = join8(path2, self._index[i]);
19488
19488
  debug('stat "%s"', p);
19489
19489
  fs.stat(p, function(err2, stat) {
19490
19490
  if (err2) return next(err2);
@@ -21908,7 +21908,7 @@ var require_response = __commonJS({
21908
21908
  var encodeUrl = require_encodeurl();
21909
21909
  var escapeHtml = require_escape_html();
21910
21910
  var http = __require("http");
21911
- var isAbsolute = require_utils2().isAbsolute;
21911
+ var isAbsolute2 = require_utils2().isAbsolute;
21912
21912
  var onFinished = require_on_finished();
21913
21913
  var path = __require("path");
21914
21914
  var statuses = require_statuses();
@@ -22114,7 +22114,7 @@ var require_response = __commonJS({
22114
22114
  done = options;
22115
22115
  opts = {};
22116
22116
  }
22117
- if (!opts.root && !isAbsolute(path2)) {
22117
+ if (!opts.root && !isAbsolute2(path2)) {
22118
22118
  throw new TypeError("path must be absolute or specify root to res.sendFile");
22119
22119
  }
22120
22120
  var pathname = encodeURI(path2);
@@ -24860,7 +24860,7 @@ var require_websocket = __commonJS({
24860
24860
  var http = __require("http");
24861
24861
  var net2 = __require("net");
24862
24862
  var tls = __require("tls");
24863
- var { randomBytes, createHash: createHash4 } = __require("crypto");
24863
+ var { randomBytes, createHash: createHash5 } = __require("crypto");
24864
24864
  var { Duplex, Readable } = __require("stream");
24865
24865
  var { URL: URL2 } = __require("url");
24866
24866
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -25528,7 +25528,7 @@ var require_websocket = __commonJS({
25528
25528
  abortHandshake(websocket, socket, "Invalid Upgrade header");
25529
25529
  return;
25530
25530
  }
25531
- const digest = createHash4("sha1").update(key + GUID).digest("base64");
25531
+ const digest = createHash5("sha1").update(key + GUID).digest("base64");
25532
25532
  if (res.headers["sec-websocket-accept"] !== digest) {
25533
25533
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
25534
25534
  return;
@@ -25809,7 +25809,7 @@ var require_stream = __commonJS({
25809
25809
  };
25810
25810
  duplex._final = function(callback) {
25811
25811
  if (ws.readyState === ws.CONNECTING) {
25812
- ws.once("open", function open2() {
25812
+ ws.once("open", function open3() {
25813
25813
  duplex._final(callback);
25814
25814
  });
25815
25815
  return;
@@ -25830,7 +25830,7 @@ var require_stream = __commonJS({
25830
25830
  };
25831
25831
  duplex._write = function(chunk, encoding, callback) {
25832
25832
  if (ws.readyState === ws.CONNECTING) {
25833
- ws.once("open", function open2() {
25833
+ ws.once("open", function open3() {
25834
25834
  duplex._write(chunk, encoding, callback);
25835
25835
  });
25836
25836
  return;
@@ -25897,7 +25897,7 @@ var require_websocket_server = __commonJS({
25897
25897
  var EventEmitter = __require("events");
25898
25898
  var http = __require("http");
25899
25899
  var { Duplex } = __require("stream");
25900
- var { createHash: createHash4 } = __require("crypto");
25900
+ var { createHash: createHash5 } = __require("crypto");
25901
25901
  var extension2 = require_extension();
25902
25902
  var PerMessageDeflate2 = require_permessage_deflate();
25903
25903
  var subprotocol2 = require_subprotocol();
@@ -26204,7 +26204,7 @@ var require_websocket_server = __commonJS({
26204
26204
  );
26205
26205
  }
26206
26206
  if (this._state > RUNNING) return abortHandshake(socket, 503);
26207
- const digest = createHash4("sha1").update(key + GUID).digest("base64");
26207
+ const digest = createHash5("sha1").update(key + GUID).digest("base64");
26208
26208
  const headers = [
26209
26209
  "HTTP/1.1 101 Switching Protocols",
26210
26210
  "Upgrade: websocket",
@@ -27777,9 +27777,9 @@ var A2AServer = class {
27777
27777
  };
27778
27778
 
27779
27779
  // src/index.ts
27780
- import { randomUUID as randomUUID7 } from "crypto";
27780
+ import { randomUUID as randomUUID8 } from "crypto";
27781
27781
  import { readFileSync as readFileSync5 } from "node:fs";
27782
- import { dirname as dirname4, resolve as resolve3 } from "node:path";
27782
+ import { dirname as dirname5, resolve as resolve3 } from "node:path";
27783
27783
  import { fileURLToPath as fileURLToPath3 } from "node:url";
27784
27784
 
27785
27785
  // src/task-context.ts
@@ -28249,7 +28249,7 @@ var ChatStore = class {
28249
28249
  }
28250
28250
  /** Ensure the conv directory exists. Idempotent. */
28251
28251
  async ensureDir(convId) {
28252
- await mkdir(this.dirFor(convId), { recursive: true });
28252
+ await mkdir(this.dirFor(convId), { recursive: true, mode: 448 });
28253
28253
  }
28254
28254
  /**
28255
28255
  * Write metadata for a conversation. Called on `channel_join` so the agent
@@ -28303,7 +28303,13 @@ var ChatStore = class {
28303
28303
  }
28304
28304
  });
28305
28305
  if (duplicate) return;
28306
- await appendFile(this.jsonlPath(convId), JSON.stringify(msg) + "\n");
28306
+ const file = await open(this.jsonlPath(convId), "a", 384);
28307
+ try {
28308
+ await file.writeFile((raw && !raw.endsWith("\n") ? "\n" : "") + JSON.stringify(msg) + "\n");
28309
+ await file.sync();
28310
+ } finally {
28311
+ await file.close();
28312
+ }
28307
28313
  appended = true;
28308
28314
  });
28309
28315
  this.#appendQueues.set(convId, current);
@@ -28451,8 +28457,216 @@ function isNotFound(err) {
28451
28457
  return !!err && typeof err === "object" && err.code === "ENOENT";
28452
28458
  }
28453
28459
 
28460
+ // src/chat-turns.ts
28461
+ import { join as join3 } from "node:path";
28462
+
28463
+ // src/private-chat-records.ts
28464
+ import { createHash as createHash2, randomUUID as randomUUID5 } from "node:crypto";
28465
+ import { mkdir as mkdir2, open as open2, readdir, readFile as readFile2, rename, unlink } from "node:fs/promises";
28466
+ import { join as join2 } from "node:path";
28467
+ var PrivateChatRecords = class {
28468
+ constructor(root) {
28469
+ this.root = root;
28470
+ }
28471
+ root;
28472
+ key(value) {
28473
+ return createHash2("sha256").update(value).digest("hex");
28474
+ }
28475
+ path(key) {
28476
+ if (!/^[a-f0-9]{64}$/.test(key)) throw new Error("CHAT_RECORD_KEY_INVALID");
28477
+ return join2(this.root, `${key}.json`);
28478
+ }
28479
+ async get(key) {
28480
+ try {
28481
+ return JSON.parse(await readFile2(this.path(key), "utf8"));
28482
+ } catch (error) {
28483
+ if (error.code === "ENOENT") return null;
28484
+ throw new Error("CHAT_RECORD_READ_FAILED");
28485
+ }
28486
+ }
28487
+ async put(key, value) {
28488
+ await mkdir2(this.root, { recursive: true, mode: 448 });
28489
+ const target = this.path(key);
28490
+ const tmp = `${target}.${randomUUID5()}.tmp`;
28491
+ const file = await open2(tmp, "wx", 384);
28492
+ try {
28493
+ await file.writeFile(JSON.stringify(value));
28494
+ await file.sync();
28495
+ } finally {
28496
+ await file.close();
28497
+ }
28498
+ await rename(tmp, target);
28499
+ const dir = await open2(this.root, "r");
28500
+ try {
28501
+ await dir.sync();
28502
+ } finally {
28503
+ await dir.close();
28504
+ }
28505
+ }
28506
+ async remove(key) {
28507
+ await unlink(this.path(key)).catch((error) => {
28508
+ if (error.code !== "ENOENT") throw new Error("CHAT_RECORD_DELETE_FAILED");
28509
+ });
28510
+ }
28511
+ async entries() {
28512
+ const names = await readdir(this.root).catch((error) => {
28513
+ if (error.code === "ENOENT") return [];
28514
+ throw new Error("CHAT_RECORD_LIST_FAILED");
28515
+ });
28516
+ const result = [];
28517
+ for (const name of names.sort()) {
28518
+ if (!/^[a-f0-9]{64}\.json$/.test(name)) continue;
28519
+ const key = name.slice(0, -5);
28520
+ const record = await this.get(key);
28521
+ if (record) result.push([key, record]);
28522
+ }
28523
+ return result;
28524
+ }
28525
+ };
28526
+
28527
+ // src/chat-turns.ts
28528
+ function waitForChatTurn(work, signal) {
28529
+ if (!signal) return work;
28530
+ return new Promise((resolve4, reject) => {
28531
+ const abort = () => reject(new Error("CHAT_WAIT_ABORTED"));
28532
+ if (signal.aborted) {
28533
+ abort();
28534
+ return;
28535
+ }
28536
+ signal.addEventListener("abort", abort, { once: true });
28537
+ work.then(resolve4, reject).finally(() => signal.removeEventListener("abort", abort));
28538
+ });
28539
+ }
28540
+ var ChatTurns = class {
28541
+ constructor(options) {
28542
+ this.options = options;
28543
+ this.records = new PrivateChatRecords(join3(options.root, ".turns", new PrivateChatRecords("").key(options.owner)));
28544
+ }
28545
+ options;
28546
+ records;
28547
+ jobs = /* @__PURE__ */ new Map();
28548
+ tails = /* @__PURE__ */ new Map();
28549
+ active = 0;
28550
+ slots = [];
28551
+ ready;
28552
+ start() {
28553
+ return this.ready ??= this.recover();
28554
+ }
28555
+ async recover() {
28556
+ const entries = await this.records.entries();
28557
+ entries.sort((a, b) => a[1].frame.timestamp.localeCompare(b[1].frame.timestamp));
28558
+ for (const [key, turn] of entries) {
28559
+ if (turn.phase === "running" || turn.phase === "recovering") {
28560
+ turn.phase = turn.runId ? "recovering" : "uncertain";
28561
+ await this.records.put(key, turn);
28562
+ }
28563
+ if (turn.phase === "uncertain") {
28564
+ this.options.progress(turn.frame, "uncertain");
28565
+ } else if (turn.phase !== "completed" || turn.text !== void 0) {
28566
+ this.schedule(key, turn);
28567
+ }
28568
+ }
28569
+ }
28570
+ async submit(frame, signal) {
28571
+ await this.start();
28572
+ const key = this.records.key(`${frame.convId}:${frame.id}`);
28573
+ let job = this.jobs.get(key);
28574
+ if (!job) {
28575
+ job = this.admit(key, frame);
28576
+ this.jobs.set(key, job);
28577
+ void job.finally(() => {
28578
+ if (this.jobs.get(key) === job) this.jobs.delete(key);
28579
+ }).catch(() => {
28580
+ });
28581
+ }
28582
+ return waitForChatTurn(job, signal);
28583
+ }
28584
+ async admit(key, frame) {
28585
+ const existing = await this.records.get(key);
28586
+ if (existing) {
28587
+ if (existing.phase === "completed") return;
28588
+ if (existing.phase === "uncertain") {
28589
+ this.options.progress(frame, "uncertain");
28590
+ return;
28591
+ }
28592
+ }
28593
+ const turn = existing ?? { frame, phase: "accepted" };
28594
+ if (!existing) await this.records.put(key, turn);
28595
+ this.options.progress(frame, turn.phase);
28596
+ await this.queue(key, turn);
28597
+ }
28598
+ schedule(key, turn) {
28599
+ const job = this.queue(key, turn);
28600
+ this.jobs.set(key, job);
28601
+ void job.finally(() => {
28602
+ if (this.jobs.get(key) === job) this.jobs.delete(key);
28603
+ }).catch(() => {
28604
+ });
28605
+ }
28606
+ queue(key, turn) {
28607
+ const previous = this.tails.get(turn.frame.convId) ?? Promise.resolve();
28608
+ const job = previous.catch(() => {
28609
+ }).then(() => this.run(key, turn));
28610
+ this.tails.set(turn.frame.convId, job);
28611
+ void job.finally(() => {
28612
+ if (this.tails.get(turn.frame.convId) === job) this.tails.delete(turn.frame.convId);
28613
+ }).catch(() => {
28614
+ });
28615
+ return job;
28616
+ }
28617
+ async run(key, turn) {
28618
+ if (this.active >= (this.options.concurrency ?? 4)) await new Promise((resolve4) => this.slots.push(resolve4));
28619
+ this.active += 1;
28620
+ const progress = () => this.options.progress(turn.frame, turn.phase);
28621
+ const timer = setInterval(progress, this.options.progressMs ?? 3e4);
28622
+ timer.unref?.();
28623
+ try {
28624
+ if (turn.phase !== "completed") {
28625
+ turn.phase = turn.runId ? "recovering" : "running";
28626
+ await this.records.put(key, turn);
28627
+ progress();
28628
+ const text = await this.options.execute(turn.frame, {
28629
+ runId: turn.runId,
28630
+ saveRunId: async (id) => {
28631
+ turn.runId = id;
28632
+ await this.records.put(key, turn);
28633
+ }
28634
+ });
28635
+ turn.text = text ?? void 0;
28636
+ turn.completedAt = (/* @__PURE__ */ new Date()).toISOString();
28637
+ turn.phase = "completed";
28638
+ await this.records.put(key, turn);
28639
+ }
28640
+ if (turn.text !== void 0) await this.options.reply(turn, `chat-${key}`);
28641
+ turn.text = void 0;
28642
+ await this.records.put(key, turn);
28643
+ progress();
28644
+ } catch (error) {
28645
+ if (turn.phase !== "completed") {
28646
+ const code = error instanceof Error ? error.message : "";
28647
+ if (["CHAT_RUNTIME_RUN_FAILED", "CHAT_RUNTIME_EMPTY_FINAL", "CHAT_RUNTIME_HTTP_404", "CHAT_RUNTIME_INVALID_STATUS"].includes(code)) {
28648
+ turn.runId = void 0;
28649
+ }
28650
+ turn.phase = turn.runId ? "recovering" : "uncertain";
28651
+ await this.records.put(key, turn).catch(() => {
28652
+ });
28653
+ }
28654
+ progress();
28655
+ this.options.error("CHAT_TURN_PENDING_RECONCILIATION");
28656
+ if (turn.phase === "completed" || turn.runId) {
28657
+ const retry = setTimeout(() => this.schedule(key, turn), 15e3);
28658
+ retry.unref?.();
28659
+ }
28660
+ } finally {
28661
+ clearInterval(timer);
28662
+ this.active -= 1;
28663
+ this.slots.shift()?.();
28664
+ }
28665
+ }
28666
+ };
28667
+
28454
28668
  // src/openclaw-supervisor-client.ts
28455
- import { randomUUID as randomUUID5 } from "node:crypto";
28669
+ import { randomUUID as randomUUID6 } from "node:crypto";
28456
28670
  import { spawn } from "node:child_process";
28457
28671
  import { closeSync, existsSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync3, realpathSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
28458
28672
  import { homedir as homedir3 } from "node:os";
@@ -28461,30 +28675,30 @@ import { dirname as dirname2, resolve } from "node:path";
28461
28675
  import { fileURLToPath } from "node:url";
28462
28676
 
28463
28677
  // src/supervisor-protocol.ts
28464
- import { createHash as createHash2 } from "node:crypto";
28678
+ import { createHash as createHash3 } from "node:crypto";
28465
28679
  import { chmodSync, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "node:fs";
28466
28680
  import { tmpdir } from "node:os";
28467
- import { dirname, join as join2 } from "node:path";
28681
+ import { dirname, join as join4 } from "node:path";
28468
28682
  var SUPERVISOR_PROTOCOL_VERSION = 2;
28469
28683
  var SUPERVISOR_RENEW_MS = 5e3;
28470
28684
  var CURRENT_PROCESS_EPOCH_MS = Math.floor(Date.now() - process.uptime() * 1e3);
28471
28685
  function supervisorOwnerHash(lifecycleKey) {
28472
- return createHash2("sha256").update(lifecycleKey).digest("hex");
28686
+ return createHash3("sha256").update(lifecycleKey).digest("hex");
28473
28687
  }
28474
28688
  function supervisorDir(stateDir) {
28475
- return join2(stateDir, "perkos-a2a", "supervisors");
28689
+ return join4(stateDir, "perkos-a2a", "supervisors");
28476
28690
  }
28477
28691
  function supervisorSocketPath(stateDir, ownerHash) {
28478
- const preferred = join2(supervisorDir(stateDir), `${ownerHash.slice(0, 32)}.sock`);
28692
+ const preferred = join4(supervisorDir(stateDir), `${ownerHash.slice(0, 32)}.sock`);
28479
28693
  if (Buffer.byteLength(preferred) <= 96) return preferred;
28480
28694
  const uid = typeof process.getuid === "function" ? process.getuid() : "user";
28481
- return join2(tmpdir(), `perkos-a2a-${uid}`, `${ownerHash.slice(0, 32)}.sock`);
28695
+ return join4(tmpdir(), `perkos-a2a-${uid}`, `${ownerHash.slice(0, 32)}.sock`);
28482
28696
  }
28483
28697
  function supervisorBootstrapPath(stateDir, ownerHash) {
28484
- return join2(supervisorDir(stateDir), `${ownerHash}.bootstrap.json`);
28698
+ return join4(supervisorDir(stateDir), `${ownerHash}.bootstrap.json`);
28485
28699
  }
28486
28700
  function supervisorRolloverLockPath(stateDir, ownerHash) {
28487
- return join2(supervisorDir(stateDir), `${ownerHash}.rollover.lock`);
28701
+ return join4(supervisorDir(stateDir), `${ownerHash}.rollover.lock`);
28488
28702
  }
28489
28703
  function currentBootId() {
28490
28704
  try {
@@ -28601,7 +28815,7 @@ var OpenClawSupervisorClient = class {
28601
28815
  return this.requestWithMessage({
28602
28816
  protocolVersion: 2,
28603
28817
  type: "claim",
28604
- requestId: randomUUID5(),
28818
+ requestId: randomUUID6(),
28605
28819
  lease: this.freshLease(),
28606
28820
  config: this.config
28607
28821
  }, 15e3);
@@ -28626,7 +28840,7 @@ var OpenClawSupervisorClient = class {
28626
28840
  const stopped = await this.requestWithMessage({
28627
28841
  protocolVersion: 2,
28628
28842
  type: "release",
28629
- requestId: randomUUID5(),
28843
+ requestId: randomUUID6(),
28630
28844
  lease: this.freshLease()
28631
28845
  }, 8e3);
28632
28846
  if (stopped?.state !== "STOPPED") throw new Error("OWNER_STOP_TIMEOUT:legacy supervisor did not return STOPPED");
@@ -28635,7 +28849,7 @@ var OpenClawSupervisorClient = class {
28635
28849
  const stopped = await this.requestWithMessage({
28636
28850
  protocolVersion: 2,
28637
28851
  type: "prepareRollover",
28638
- requestId: randomUUID5(),
28852
+ requestId: randomUUID6(),
28639
28853
  lease: this.freshLease(),
28640
28854
  targetVersion: this.config.version,
28641
28855
  targetInstallRoot: this.config.installRoot
@@ -28747,7 +28961,7 @@ var OpenClawSupervisorClient = class {
28747
28961
  this.renewTimer = null;
28748
28962
  const stopping = (async () => {
28749
28963
  if (this.socket && !this.socket.destroyed) {
28750
- const requestId = randomUUID5();
28964
+ const requestId = randomUUID6();
28751
28965
  try {
28752
28966
  await this.requestWithMessage({
28753
28967
  protocolVersion: 2,
@@ -28779,13 +28993,16 @@ var OpenClawSupervisorClient = class {
28779
28993
  return await this.request("discover", {}, 15e3);
28780
28994
  }
28781
28995
  sendTaskResponse(original, result) {
28782
- this.write({ protocolVersion: 2, type: "relayTaskResult", lease: this.freshLease(), eventId: randomUUID5(), original, result });
28996
+ this.write({ protocolVersion: 2, type: "relayTaskResult", lease: this.freshLease(), eventId: randomUUID6(), original, result });
28783
28997
  }
28784
28998
  async sendChatReply(payload) {
28785
28999
  return await this.request("chatReply", payload, 15e3);
28786
29000
  }
29001
+ async sendChatProgress(payload) {
29002
+ await this.request("chatProgress", payload, 5e3);
29003
+ }
28787
29004
  async request(method, payload, timeoutMs) {
28788
- const requestId = randomUUID5();
29005
+ const requestId = randomUUID6();
28789
29006
  return this.requestWithMessage({ protocolVersion: 2, type: "request", requestId, lease: this.freshLease(), method, payload }, timeoutMs);
28790
29007
  }
28791
29008
  async requestWithMessage(message, timeoutMs) {
@@ -28970,7 +29187,7 @@ var OpenClawSupervisorClient = class {
28970
29187
  };
28971
29188
 
28972
29189
  // src/runtime-evidence.ts
28973
- import { createHash as createHash3, createHmac, timingSafeEqual } from "node:crypto";
29190
+ import { createHash as createHash4, createHmac, timingSafeEqual } from "node:crypto";
28974
29191
  import {
28975
29192
  chmodSync as chmodSync2,
28976
29193
  mkdirSync as mkdirSync3,
@@ -28979,7 +29196,7 @@ import {
28979
29196
  writeFileSync as writeFileSync3
28980
29197
  } from "node:fs";
28981
29198
  import { homedir as homedir4 } from "node:os";
28982
- import { join as join3, resolve as resolve2 } from "node:path";
29199
+ import { join as join5, resolve as resolve2 } from "node:path";
28983
29200
  var RUNTIME_ATTEMPT_SCHEMA_VERSION = 2;
28984
29201
  var RUNTIME_STATUS_SCHEMA_VERSION = 3;
28985
29202
  function canonicalize(value) {
@@ -29034,20 +29251,20 @@ function canonicalConfigJson(config) {
29034
29251
  return JSON.stringify({ schemaVersion: RUNTIME_STATUS_SCHEMA_VERSION, config: canonicalize(configEvidenceProjection(config)) });
29035
29252
  }
29036
29253
  function configDigest(config) {
29037
- return createHash3("sha256").update(canonicalConfigJson(config)).digest("hex");
29254
+ return createHash4("sha256").update(canonicalConfigJson(config)).digest("hex");
29038
29255
  }
29039
29256
  function configFingerprint(config, hmacKeyHex) {
29040
29257
  if (!/^[a-f0-9]{64}$/u.test(hmacKeyHex)) throw new Error("runtime evidence HMAC key is invalid");
29041
29258
  return createHmac("sha256", Buffer.from(hmacKeyHex, "hex")).update(canonicalConfigJson(config)).digest("hex");
29042
29259
  }
29043
29260
  function runtimeEvidenceDir(stateDir) {
29044
- return resolve2(stateDir ?? process.env.OPENCLAW_STATE_DIR ?? join3(homedir4(), ".openclaw"), "perkos-a2a");
29261
+ return resolve2(stateDir ?? process.env.OPENCLAW_STATE_DIR ?? join5(homedir4(), ".openclaw"), "perkos-a2a");
29045
29262
  }
29046
29263
  function runtimeAttemptPath(stateDir) {
29047
- return join3(runtimeEvidenceDir(stateDir), "active-attempt.json");
29264
+ return join5(runtimeEvidenceDir(stateDir), "active-attempt.json");
29048
29265
  }
29049
29266
  function runtimeStatusPath(stateDir) {
29050
- return join3(runtimeEvidenceDir(stateDir), "runtime-status.json");
29267
+ return join5(runtimeEvidenceDir(stateDir), "runtime-status.json");
29051
29268
  }
29052
29269
  function configAgentId(config) {
29053
29270
  if (config.platform?.agentId) return config.platform.agentId;
@@ -29239,10 +29456,186 @@ function formatProjectContext(context) {
29239
29456
  return lines;
29240
29457
  }
29241
29458
 
29459
+ // src/chat-runtime-http.ts
29460
+ import { request as httpRequest } from "node:http";
29461
+ import { request as httpsRequest } from "node:https";
29462
+ function chatRuntimeJson(url, init) {
29463
+ return new Promise((resolve4, reject) => {
29464
+ const fail = () => reject(new Error("CHAT_RUNTIME_TRANSPORT_UNCERTAIN"));
29465
+ const parsed = new URL(url);
29466
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
29467
+ fail();
29468
+ return;
29469
+ }
29470
+ const req = (parsed.protocol === "https:" ? httpsRequest : httpRequest)(parsed, {
29471
+ method: init.method,
29472
+ headers: init.headers
29473
+ }, (res) => {
29474
+ if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
29475
+ res.resume();
29476
+ reject(new Error(`CHAT_RUNTIME_HTTP_${res.statusCode ?? 0}`));
29477
+ return;
29478
+ }
29479
+ const chunks = [];
29480
+ let size = 0;
29481
+ res.on("data", (chunk) => {
29482
+ size += chunk.length;
29483
+ if (size > 16 * 1024 * 1024) {
29484
+ res.destroy();
29485
+ fail();
29486
+ return;
29487
+ }
29488
+ chunks.push(chunk);
29489
+ });
29490
+ res.on("error", fail);
29491
+ res.on("aborted", fail);
29492
+ res.on("end", () => {
29493
+ try {
29494
+ resolve4(JSON.parse(Buffer.concat(chunks).toString("utf8")));
29495
+ } catch {
29496
+ reject(new Error("CHAT_RUNTIME_INVALID_RESPONSE"));
29497
+ }
29498
+ });
29499
+ });
29500
+ req.on("error", fail);
29501
+ const connectTimer = setTimeout(() => req.destroy(new Error("CHAT_RUNTIME_CONNECT_TIMEOUT")), 15e3);
29502
+ connectTimer.unref?.();
29503
+ req.on("socket", (socket) => {
29504
+ if (!socket.connecting) clearTimeout(connectTimer);
29505
+ else socket.once(parsed.protocol === "https:" ? "secureConnect" : "connect", () => clearTimeout(connectTimer));
29506
+ });
29507
+ req.on("close", () => clearTimeout(connectTimer));
29508
+ req.end(init.body);
29509
+ });
29510
+ }
29511
+ async function runChatRuntime(url, headers, body, checkpoint, request = chatRuntimeJson, pollMs = 2e3) {
29512
+ const isRuns = new URL(url).pathname.replace(/\/$/, "").endsWith("/v1/runs");
29513
+ if (!isRuns) {
29514
+ if (checkpoint.runId) throw new Error("CHAT_RUNTIME_RECOVERY_ENDPOINT_CHANGED");
29515
+ return request(url, { method: "POST", headers, body });
29516
+ }
29517
+ let runId = checkpoint.runId;
29518
+ if (!runId) {
29519
+ const started = await request(url, { method: "POST", headers, body });
29520
+ if (typeof started?.run_id !== "string" || !started.run_id) throw new Error("CHAT_RUNTIME_RUN_ID_MISSING");
29521
+ runId = started.run_id;
29522
+ await checkpoint.saveRunId(runId);
29523
+ }
29524
+ for (; ; ) {
29525
+ const result = await request(`${url.replace(/\/$/, "")}/${encodeURIComponent(runId)}`, { method: "GET", headers });
29526
+ if (result?.status === "completed") {
29527
+ if (typeof result.output !== "string" || !result.output.trim()) throw new Error("CHAT_RUNTIME_EMPTY_FINAL");
29528
+ return { output: [{ role: "assistant", content: [{ type: "output_text", text: result.output }] }] };
29529
+ }
29530
+ if (result?.status === "failed" || result?.status === "cancelled") throw new Error("CHAT_RUNTIME_RUN_FAILED");
29531
+ if (!["started", "running", "queued", "pending", "awaiting_approval"].includes(result?.status ?? "")) throw new Error("CHAT_RUNTIME_INVALID_STATUS");
29532
+ await new Promise((resolve4) => setTimeout(resolve4, pollMs));
29533
+ }
29534
+ }
29535
+
29242
29536
  // src/bridge-chat-reply.ts
29537
+ function hermesDeliveryUrl(baseUrl, endpoint, profile) {
29538
+ const base = baseUrl.replace(/\/+$/, "");
29539
+ const prefix = profile ? `/p/${encodeURIComponent(profile)}` : "";
29540
+ const ep = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
29541
+ return `${base}${prefix}${ep}`;
29542
+ }
29543
+ function personaInstructions() {
29544
+ const b64 = process.env.PERKOS_AGENT_SOUL_B64 || "";
29545
+ if (!b64.trim()) return "";
29546
+ try {
29547
+ return Buffer.from(b64, "base64").toString("utf8").trim();
29548
+ } catch {
29549
+ return "";
29550
+ }
29551
+ }
29243
29552
  function usesHttpDelivery(runtimeKind) {
29244
29553
  return runtimeKind === "hermes" || runtimeKind === "hermes-api" || runtimeKind === "zeroclaw" || runtimeKind === "custom";
29245
29554
  }
29555
+ async function deliverToHermes(config, message, wantResponse = false, sessionKeyOverride, chatCheckpoint) {
29556
+ const runtime2 = config.runtime || {};
29557
+ const baseUrl = (runtime2.hermesUrl || process.env.HERMES_API_URL || process.env.HERMES_WORKSPACE_URL || "http://127.0.0.1:8642").replace(/\/+$/, "");
29558
+ const endpoint = runtime2.hermesEndpoint || process.env.HERMES_API_ENDPOINT || process.env.HERMES_WORKSPACE_ENDPOINT || "/v1/responses";
29559
+ const sessionKey = sessionKeyOverride || runtime2.sessionKey || process.env.HERMES_SESSION_ID || process.env.HERMES_SESSION_KEY || "a2a";
29560
+ const token = runtime2.hermesToken || process.env.API_SERVER_KEY || process.env.HERMES_API_KEY || process.env.HERMES_WORKSPACE_TOKEN || process.env.HERMES_SESSION_TOKEN;
29561
+ const profile = runtime2.hermesProfile || process.env.PERKOS_HERMES_PROFILE || "";
29562
+ const url = hermesDeliveryUrl(baseUrl, endpoint, profile);
29563
+ const headers = {
29564
+ "content-type": "application/json",
29565
+ "x-hermes-session-id": sessionKey,
29566
+ "x-hermes-session-key": sessionKey
29567
+ };
29568
+ if (token) headers.authorization = `Bearer ${token}`;
29569
+ const normalizedEndpoint = endpoint.replace(/^\/?/, "/");
29570
+ const persona = personaInstructions();
29571
+ const model = runtime2.model?.trim() || process.env.HERMES_MODEL?.trim();
29572
+ const body = normalizedEndpoint.startsWith("/webhook") ? { message } : normalizedEndpoint.startsWith("/v1/chat/completions") ? {
29573
+ ...model ? { model } : {},
29574
+ messages: [
29575
+ ...persona ? [{ role: "system", content: persona }] : [],
29576
+ { role: "user", content: message }
29577
+ ],
29578
+ stream: false
29579
+ } : normalizedEndpoint.startsWith("/v1/runs") ? { input: message, session_id: sessionKey, ...persona ? { instructions: persona } : {} } : normalizedEndpoint.startsWith("/v1/responses") ? { ...model ? { model } : {}, input: message, store: true, ...persona ? { instructions: persona } : {} } : { sessionKey, message };
29580
+ if (chatCheckpoint) return runChatRuntime(url, headers, JSON.stringify(body), chatCheckpoint);
29581
+ const timeoutMs = Math.max(
29582
+ 1e3,
29583
+ Number(process.env.PERKOS_RUNTIME_DELIVERY_TIMEOUT_MS) || 24e4
29584
+ );
29585
+ const response = await fetch(url, {
29586
+ method: "POST",
29587
+ headers,
29588
+ body: JSON.stringify(body),
29589
+ signal: AbortSignal.timeout(timeoutMs)
29590
+ });
29591
+ if (!response.ok) {
29592
+ await response.body?.cancel();
29593
+ throw new Error(`Hermes API delivery failed (${response.status})`);
29594
+ }
29595
+ if (!wantResponse) return void 0;
29596
+ return await response.json();
29597
+ }
29598
+ function chatSessionKey(convId) {
29599
+ const safe2 = convId.replace(/[^a-zA-Z0-9_.:-]/g, "-").slice(0, 160);
29600
+ return `perkos-chat:${safe2 || "unknown"}`;
29601
+ }
29602
+ function autoReplyEnabled() {
29603
+ return String(process.env.A2A_HERMES_AUTO_REPLY || "").toLowerCase() === "true";
29604
+ }
29605
+ function extractAssistantText(resp) {
29606
+ if (!resp || typeof resp !== "object") return null;
29607
+ const r = resp;
29608
+ const output = r.output;
29609
+ if (Array.isArray(output)) {
29610
+ for (const item of [...output].reverse()) {
29611
+ if (!item || typeof item !== "object") continue;
29612
+ if (item.role && item.role !== "assistant") continue;
29613
+ if (item.type && item.type !== "message") continue;
29614
+ if (item.status && item.status !== "completed") continue;
29615
+ const content = item.content;
29616
+ if (!Array.isArray(content)) continue;
29617
+ const text = [];
29618
+ for (const part of content) {
29619
+ if (!part || typeof part !== "object") continue;
29620
+ const p = part;
29621
+ if ((!p.type || p.type === "output_text" || p.type === "text") && typeof p.text === "string" && p.text.trim()) text.push(p.text);
29622
+ }
29623
+ if (text.length) return text.join("\n");
29624
+ }
29625
+ }
29626
+ if (typeof r.response === "string" && r.response.trim()) return r.response;
29627
+ const choices = r.choices;
29628
+ if (Array.isArray(choices)) {
29629
+ for (const choice of choices) {
29630
+ if (!choice || typeof choice !== "object") continue;
29631
+ const message = choice.message;
29632
+ if (!message || typeof message !== "object") continue;
29633
+ const content = message.content;
29634
+ if (typeof content === "string" && content.trim()) return content;
29635
+ }
29636
+ }
29637
+ return null;
29638
+ }
29246
29639
 
29247
29640
  // src/platform-control-plane.ts
29248
29641
  function resolvePlatformControlPlane(config, env = process.env) {
@@ -29310,12 +29703,67 @@ async function tryHandleInstalledVoiceMarker(message, config, loadVoice = defaul
29310
29703
 
29311
29704
  // src/a2a-maintenance.ts
29312
29705
  import { spawn as spawn2 } from "node:child_process";
29313
- import { chmod, mkdir as mkdir2, readdir, readFile as readFile2, rename, unlink, writeFile as writeFile2 } from "node:fs/promises";
29706
+ import { readdir as readdir2, readFile as readFile3, unlink as unlink2 } from "node:fs/promises";
29314
29707
  import { homedir as homedir5 } from "node:os";
29315
- import { dirname as dirname3, join as join4 } from "node:path";
29708
+ import { join as join6 } from "node:path";
29316
29709
  import { fileURLToPath as fileURLToPath2 } from "node:url";
29317
- var PERKOS_A2A_UPDATE_PREFIX = "PERKOS_A2A_UPDATE:";
29710
+
29711
+ // src/service-environment.ts
29712
+ import { delimiter, dirname as dirname3, isAbsolute } from "node:path";
29713
+ function serviceEnvironment(nodePath = process.execPath, inherited = process.env) {
29714
+ const paths = [
29715
+ dirname3(nodePath),
29716
+ ...(inherited.PATH ?? "").split(delimiter),
29717
+ "/usr/bin",
29718
+ "/bin",
29719
+ "/usr/sbin",
29720
+ "/sbin"
29721
+ ];
29722
+ return { ...inherited, PATH: [...new Set(paths.filter(isAbsolute))].join(delimiter) };
29723
+ }
29724
+
29725
+ // src/maintenance-result.ts
29726
+ import { chmod, mkdir as mkdir3, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
29727
+ import { dirname as dirname4 } from "node:path";
29728
+ var MAINTENANCE_ERRORS = [
29729
+ "npx_unavailable",
29730
+ "npx_exec_failed",
29731
+ "invalid_update_receipt",
29732
+ "update_failed",
29733
+ "managed_install_not_found",
29734
+ "managed_config_invalid",
29735
+ "managed_identity_mismatch",
29736
+ "bridge_install_missing",
29737
+ "bridge_version_mismatch"
29738
+ ];
29739
+ var TARGET_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
29318
29740
  var REQUEST_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
29741
+ function safeMaintenanceError(value) {
29742
+ return typeof value === "string" && MAINTENANCE_ERRORS.includes(value) ? value : "update_failed";
29743
+ }
29744
+ function parseMaintenanceResult(value) {
29745
+ if (!value || typeof value !== "object") return null;
29746
+ const v = value;
29747
+ if (v.state === "completed" && typeof v.installedVersion === "string" && TARGET_VERSION.test(v.installedVersion)) {
29748
+ return { schemaVersion: 2, state: "completed", installedVersion: v.installedVersion };
29749
+ }
29750
+ if (v.state === "failed") return { schemaVersion: 2, state: "failed", errorCode: safeMaintenanceError(v.errorCode) };
29751
+ return null;
29752
+ }
29753
+ function maintenanceReportBody(result) {
29754
+ return result.state === "completed" ? { state: result.state, installedVersion: result.installedVersion } : { state: result.state, errorCode: result.errorCode };
29755
+ }
29756
+ async function atomicPrivateJson(path, value) {
29757
+ await mkdir3(dirname4(path), { recursive: true, mode: 448 });
29758
+ const temporary = `${path}.${process.pid}.tmp`;
29759
+ await writeFile2(temporary, `${JSON.stringify(value)}
29760
+ `, { mode: 384 });
29761
+ await chmod(temporary, 384);
29762
+ await rename2(temporary, path);
29763
+ }
29764
+
29765
+ // src/a2a-maintenance.ts
29766
+ var PERKOS_A2A_UPDATE_PREFIX = "PERKOS_A2A_UPDATE:";
29319
29767
  var BRIDGE_INSTANCE_ID = REQUEST_ID;
29320
29768
  function parseA2AUpdateMarker(message) {
29321
29769
  const marker = message.trim();
@@ -29325,15 +29773,7 @@ function parseA2AUpdateMarker(message) {
29325
29773
  }
29326
29774
  var maintenanceControlPlane = resolvePlatformControlPlane;
29327
29775
  function maintenanceRoot(agentId) {
29328
- return join4(homedir5(), ".perkos", "a2a", "hermes", agentId, "maintenance");
29329
- }
29330
- async function atomicPrivateJson(path, value) {
29331
- await mkdir2(dirname3(path), { recursive: true, mode: 448 });
29332
- const temporary = `${path}.${process.pid}.tmp`;
29333
- await writeFile2(temporary, `${JSON.stringify(value)}
29334
- `, { mode: 384 });
29335
- await chmod(temporary, 384);
29336
- await rename(temporary, path);
29776
+ return join6(homedir5(), ".perkos", "a2a", "hermes", agentId, "maintenance");
29337
29777
  }
29338
29778
  async function authorizedJson(url, relayApiKey, init = {}) {
29339
29779
  const response = await fetch(url, {
@@ -29363,12 +29803,20 @@ async function claimRequest(target, requestId, bridgeInstanceId) {
29363
29803
  }
29364
29804
  async function queueHermesMaintenanceJob(job) {
29365
29805
  const root = maintenanceRoot(job.agentId);
29366
- const jobPath = join4(root, "jobs", `${job.requestId}.json`);
29806
+ const jobPath = join6(root, "jobs", `${job.requestId}.json`);
29367
29807
  await atomicPrivateJson(jobPath, job);
29368
29808
  const runnerPath = fileURLToPath2(new URL("./maintenance-runner.js", import.meta.url));
29369
29809
  const child = spawn2(process.execPath, [runnerPath, "--job", jobPath], {
29370
29810
  detached: true,
29371
- stdio: "ignore"
29811
+ stdio: "ignore",
29812
+ env: serviceEnvironment()
29813
+ });
29814
+ await new Promise((resolve4, reject) => {
29815
+ child.once("spawn", resolve4);
29816
+ child.once("error", () => reject(new Error("runner_spawn_failed")));
29817
+ }).catch(async () => {
29818
+ await unlink2(jobPath).catch(() => void 0);
29819
+ throw new Error("runner_spawn_failed");
29372
29820
  });
29373
29821
  child.unref();
29374
29822
  }
@@ -29399,40 +29847,118 @@ async function tryHandleA2AMaintenanceMarker(message, config, deps = {}) {
29399
29847
  return "PERKOS_A2A_UPDATE_FAILED:request_rejected";
29400
29848
  }
29401
29849
  }
29402
- async function reportReceipt(receipt, target) {
29850
+ async function reportReceipt(requestId, receipt, target) {
29403
29851
  await authorizedJson(
29404
- `${target.apiBaseUrl}/agents/${encodeURIComponent(target.agentId)}/maintenance/a2a-update/${receipt.requestId}/report`,
29852
+ `${target.apiBaseUrl}/agents/${encodeURIComponent(target.agentId)}/maintenance/a2a-update/${requestId}/report`,
29405
29853
  target.relayApiKey,
29406
29854
  {
29407
29855
  method: "POST",
29408
- body: JSON.stringify({
29409
- state: receipt.state,
29410
- ...receipt.installedVersion ? { installedVersion: receipt.installedVersion } : {},
29411
- ...receipt.errorCode ? { errorCode: receipt.errorCode } : {}
29412
- })
29856
+ body: JSON.stringify(maintenanceReportBody(receipt))
29413
29857
  }
29414
29858
  );
29415
29859
  }
29416
- async function flushMaintenanceReceipts(config) {
29860
+ async function flushMaintenanceReceipts(config, root) {
29417
29861
  const target = maintenanceControlPlane(config);
29418
29862
  if (!target) return;
29419
- const receiptsDir = join4(maintenanceRoot(target.agentId), "receipts");
29420
- const files = await readdir(receiptsDir).catch(() => []);
29863
+ const privateRoot = root ?? maintenanceRoot(target.agentId);
29864
+ const receiptsDir = join6(privateRoot, "receipts");
29865
+ const files = await readdir2(receiptsDir).catch(() => []);
29421
29866
  for (const file of files) {
29422
- if (!file.endsWith(".json")) continue;
29423
- const path = join4(receiptsDir, file);
29867
+ if (!file.endsWith(".json") || !REQUEST_ID.test(file.slice(0, -5))) continue;
29868
+ const path = join6(receiptsDir, file);
29424
29869
  try {
29425
- const receipt = JSON.parse(await readFile2(path, "utf8"));
29426
- if (receipt.agentId !== target.agentId || receipt.apiBaseUrl !== target.apiBaseUrl || !REQUEST_ID.test(receipt.requestId)) continue;
29427
- await reportReceipt(receipt, target);
29428
- await unlink(path);
29870
+ const stored = JSON.parse(await readFile3(path, "utf8"));
29871
+ const routingPath = join6(privateRoot, "pending-reports", file);
29872
+ const routing = stored.schemaVersion === 1 ? stored : stored.schemaVersion === 2 ? JSON.parse(await readFile3(routingPath, "utf8")) : null;
29873
+ if (!routing || routing.agentId !== target.agentId || routing.apiBaseUrl !== target.apiBaseUrl || routing.requestId !== file.slice(0, -5)) continue;
29874
+ const receipt = parseMaintenanceResult(stored);
29875
+ if (!receipt) continue;
29876
+ if (stored.schemaVersion === 1) {
29877
+ await atomicPrivateJson(routingPath, {
29878
+ schemaVersion: 1,
29879
+ requestId: routing.requestId,
29880
+ agentId: routing.agentId,
29881
+ apiBaseUrl: routing.apiBaseUrl
29882
+ });
29883
+ await atomicPrivateJson(path, receipt);
29884
+ }
29885
+ await reportReceipt(file.slice(0, -5), receipt, target);
29886
+ await unlink2(path);
29887
+ await unlink2(routingPath).catch(() => void 0);
29429
29888
  } catch {
29430
29889
  }
29431
29890
  }
29432
29891
  }
29433
29892
 
29434
29893
  // src/chat-client.ts
29435
- import { randomUUID as randomUUID6 } from "node:crypto";
29894
+ import { randomUUID as randomUUID7 } from "node:crypto";
29895
+
29896
+ // src/chat-outbox.ts
29897
+ import { join as join7 } from "node:path";
29898
+ var ChatOutbox = class {
29899
+ constructor(store, owner) {
29900
+ this.store = store;
29901
+ this.records = new PrivateChatRecords(join7(store.root, ".outbox", new PrivateChatRecords("").key(owner)));
29902
+ }
29903
+ store;
29904
+ records;
29905
+ pending = /* @__PURE__ */ new Map();
29906
+ nextAttempt = /* @__PURE__ */ new Map();
29907
+ attempts = /* @__PURE__ */ new Map();
29908
+ serial = Promise.resolve();
29909
+ loaded = false;
29910
+ lock(fn) {
29911
+ const work = this.serial.catch(() => {
29912
+ }).then(fn);
29913
+ this.serial = work;
29914
+ return work;
29915
+ }
29916
+ async load() {
29917
+ if (this.loaded) return;
29918
+ for (const [key, value] of await this.records.entries()) this.pending.set(key, value);
29919
+ this.loaded = true;
29920
+ }
29921
+ enqueue(entry) {
29922
+ return this.lock(async () => {
29923
+ await this.load();
29924
+ const key = this.records.key(`${entry.frame.convId}:${entry.frame.id}`);
29925
+ const existing = this.pending.get(key);
29926
+ if (existing) return;
29927
+ await this.records.put(key, entry);
29928
+ this.pending.set(key, entry);
29929
+ await this.store.appendIdempotent(entry.frame.convId, entry.message);
29930
+ });
29931
+ }
29932
+ flush(send, force = false) {
29933
+ return this.lock(async () => {
29934
+ await this.load();
29935
+ const sent = /* @__PURE__ */ new Set();
29936
+ for (const [key, entry] of this.pending) {
29937
+ if (!force && Date.now() < (this.nextAttempt.get(key) ?? 0)) continue;
29938
+ await this.store.appendIdempotent(entry.frame.convId, entry.message);
29939
+ if (!send(entry.frame)) break;
29940
+ sent.add(entry.frame.id);
29941
+ const attempt = (this.attempts.get(key) ?? 0) + 1;
29942
+ this.attempts.set(key, attempt);
29943
+ this.nextAttempt.set(key, Date.now() + Math.min(6e4, 5e3 * 2 ** Math.min(attempt - 1, 4)));
29944
+ }
29945
+ return sent;
29946
+ });
29947
+ }
29948
+ acknowledge(convId, id) {
29949
+ return this.lock(async () => {
29950
+ await this.load();
29951
+ const key = this.records.key(`${convId}:${id}`);
29952
+ if (!this.pending.has(key)) return;
29953
+ await this.records.remove(key);
29954
+ this.pending.delete(key);
29955
+ this.attempts.delete(key);
29956
+ this.nextAttempt.delete(key);
29957
+ });
29958
+ }
29959
+ };
29960
+
29961
+ // src/chat-client.ts
29436
29962
  var DEFAULT_URL = "wss://chat.perkos.xyz/chat";
29437
29963
  var DEFAULT_HISTORY_LIMIT = 50;
29438
29964
  var DEFAULT_MIN_RECONNECT = 1e3;
@@ -29449,6 +29975,9 @@ var ChatClient = class {
29449
29975
  handlers;
29450
29976
  logger;
29451
29977
  store;
29978
+ outbox;
29979
+ outboxTimer = null;
29980
+ progress = /* @__PURE__ */ new Map();
29452
29981
  reconnectMs;
29453
29982
  reconnectTimer = null;
29454
29983
  heartbeatTimer = null;
@@ -29477,6 +30006,7 @@ var ChatClient = class {
29477
30006
  this.handlers = opts.handlers ?? {};
29478
30007
  this.logger = this.handlers.logger ?? { info: console.log, error: console.error };
29479
30008
  this.store = new ChatStore({ storeRoot: this.config.storeRoot });
30009
+ this.outbox = new ChatOutbox(this.store, this.agentName);
29480
30010
  this.reconnectMs = this.config.minReconnectMs;
29481
30011
  }
29482
30012
  /** Storage handle — exposed so consumers can read history without going through the wire. */
@@ -29536,32 +30066,50 @@ var ChatClient = class {
29536
30066
  }
29537
30067
  /**
29538
30068
  * Send a reply from this agent into a conversation. Appends to the local
29539
- * store optimistically, then sends to the server. If the WS is offline,
29540
- * the local store is still updated and the message is dropped from the
29541
- * wire callers should re-send after reconnect if guaranteed delivery is
29542
- * required (the simple agent loop should just regenerate from history).
30069
+ * outbox and canonical history before sending. Offline replies are replayed
30070
+ * after authentication, with stable IDs, until acknowledged by the router.
30071
+ * `delivered` only means transmission attempted, not human receipt.
29543
30072
  */
29544
30073
  async sendReply(opts) {
29545
- const id = randomUUID6();
30074
+ const id = opts.id ?? randomUUID7();
29546
30075
  const msg = {
29547
30076
  id,
29548
30077
  from: `agent:${this.agentName}`,
29549
30078
  text: opts.text,
29550
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
30079
+ timestamp: opts.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
29551
30080
  replyTo: opts.replyTo ?? null
29552
30081
  };
29553
- await this.store.append(opts.convId, msg);
29554
30082
  const frame = {
29555
30083
  type: "chat_reply",
29556
30084
  id,
29557
30085
  convId: opts.convId,
29558
30086
  walletAddress: opts.walletAddress,
29559
30087
  text: opts.text,
29560
- replyTo: opts.replyTo ?? null
30088
+ replyTo: opts.replyTo ?? null,
30089
+ timestamp: msg.timestamp
29561
30090
  };
29562
- const delivered = this.sendFrame(frame);
30091
+ await this.outbox.enqueue({ frame, message: msg });
30092
+ const sent = await this.outbox.flush((pending) => this.authed && this.sendFrame(pending));
30093
+ const delivered = sent.has(id);
29563
30094
  return { id, delivered };
29564
30095
  }
30096
+ sendProgress(convId, walletAddress, replyTo, phase) {
30097
+ const key = `${convId}:${replyTo}`;
30098
+ const frame = {
30099
+ type: "typing",
30100
+ convId,
30101
+ walletAddress,
30102
+ replyTo,
30103
+ phase,
30104
+ state: phase === "completed" || phase === "uncertain" ? "stop" : "start"
30105
+ };
30106
+ if (phase === "completed") this.progress.delete(key);
30107
+ else this.progress.set(key, frame);
30108
+ if (this.authed) this.sendFrame(frame);
30109
+ }
30110
+ flushOutbox(force = false) {
30111
+ void this.outbox.flush((frame) => this.authed && this.sendFrame(frame), force).catch(() => this.logger.error("[perkos-chat] CHAT_OUTBOX_RETRY_PENDING"));
30112
+ }
29565
30113
  // -------------------------------------------------------------------------
29566
30114
  // WebSocket lifecycle
29567
30115
  // -------------------------------------------------------------------------
@@ -29613,7 +30161,7 @@ var ChatClient = class {
29613
30161
  clearTimeout(authTimer);
29614
30162
  authTimer = null;
29615
30163
  }
29616
- void this.handleFrame(frame);
30164
+ void this.handleFrame(frame).catch(() => this.logger.error("[perkos-chat] CHAT_FRAME_PROCESSING_FAILED"));
29617
30165
  });
29618
30166
  this.ws.on("close", (code, reason) => {
29619
30167
  if (authTimer) {
@@ -29640,6 +30188,11 @@ var ChatClient = class {
29640
30188
  this.setAuthed(true);
29641
30189
  this.logger.info(`[perkos-chat] authed as agent:${this.agentName}`);
29642
30190
  this.startHeartbeat();
30191
+ this.flushOutbox(true);
30192
+ for (const progress of this.progress.values()) this.sendFrame(progress);
30193
+ if (this.outboxTimer) clearInterval(this.outboxTimer);
30194
+ this.outboxTimer = setInterval(() => this.flushOutbox(), 5e3);
30195
+ this.outboxTimer.unref?.();
29643
30196
  return;
29644
30197
  case "auth_error":
29645
30198
  this.logger.error(`[perkos-chat] auth_error ${frame.code}: ${frame.message}`);
@@ -29674,6 +30227,8 @@ var ChatClient = class {
29674
30227
  await this.handleChannelJoin(frame);
29675
30228
  return;
29676
30229
  case "ack":
30230
+ await this.outbox.acknowledge(frame.convId, frame.id);
30231
+ return;
29677
30232
  case "pong":
29678
30233
  case "typing":
29679
30234
  return;
@@ -29724,8 +30279,8 @@ var ChatClient = class {
29724
30279
  };
29725
30280
  try {
29726
30281
  await this.store.appendIdempotent(frame.convId, msg);
29727
- } catch (err) {
29728
- this.logger.error(`[perkos-chat] failed to append to store: ${errMsg(err)}`);
30282
+ } catch {
30283
+ throw new Error("CHAT_INBOUND_PERSISTENCE_FAILED");
29729
30284
  }
29730
30285
  }
29731
30286
  async handleChannelJoin(frame) {
@@ -29804,11 +30359,18 @@ var ChatClient = class {
29804
30359
  messages: page.messages,
29805
30360
  hasMore: page.hasMore
29806
30361
  });
30362
+ for (const progress of this.progress.values()) {
30363
+ if (progress.convId === frame.convId) this.sendFrame(progress);
30364
+ }
29807
30365
  }
29808
30366
  sendFrame(frame) {
29809
30367
  if (this.ws && this.ws.readyState === wrapper_default.OPEN) {
29810
- this.ws.send(JSON.stringify(frame));
29811
- return true;
30368
+ try {
30369
+ this.ws.send(JSON.stringify(frame));
30370
+ return true;
30371
+ } catch {
30372
+ return false;
30373
+ }
29812
30374
  }
29813
30375
  return false;
29814
30376
  }
@@ -29828,6 +30390,10 @@ var ChatClient = class {
29828
30390
  this.reconnectMs = Math.min(delay * 2, this.config.maxReconnectMs);
29829
30391
  }
29830
30392
  clearTimers() {
30393
+ if (this.outboxTimer) {
30394
+ clearInterval(this.outboxTimer);
30395
+ this.outboxTimer = null;
30396
+ }
29831
30397
  if (this.heartbeatTimer) {
29832
30398
  clearInterval(this.heartbeatTimer);
29833
30399
  this.heartbeatTimer = null;
@@ -29903,7 +30469,7 @@ async function mutateOpenClawChatSession(api, config, sessionKey, update) {
29903
30469
  if (typeof patchSessionEntry === "function") {
29904
30470
  await patchSessionEntry({
29905
30471
  sessionKey,
29906
- fallbackEntry: { sessionId: randomUUID7(), updatedAt: Date.now() },
30472
+ fallbackEntry: { sessionId: randomUUID8(), updatedAt: Date.now() },
29907
30473
  preserveActivity: true,
29908
30474
  replaceEntry: true,
29909
30475
  update
@@ -29920,7 +30486,7 @@ async function mutateOpenClawChatSession(api, config, sessionKey, update) {
29920
30486
  await updateSessionStore(
29921
30487
  storePath,
29922
30488
  (store) => {
29923
- const current = store[sessionKey] ?? { sessionId: randomUUID7(), updatedAt: Date.now() };
30489
+ const current = store[sessionKey] ?? { sessionId: randomUUID8(), updatedAt: Date.now() };
29924
30490
  store[sessionKey] = update(current);
29925
30491
  return store[sessionKey];
29926
30492
  },
@@ -30132,12 +30698,9 @@ async function runOpenClawChatTurn(api, config, sessionKey, prompt, transcriptPr
30132
30698
  const workspaceDir = resolveAgentWorkspaceDir(runtimeConfig, agentId);
30133
30699
  const sessionContext = readOpenClawChatSession(api, agentId, sessionKey);
30134
30700
  const entry = sessionContext.entry;
30135
- const sessionId = typeof entry?.sessionId === "string" && entry.sessionId.trim() ? entry.sessionId : randomUUID7();
30701
+ const sessionId = typeof entry?.sessionId === "string" && entry.sessionId.trim() ? entry.sessionId : randomUUID8();
30136
30702
  const configuredTimeout = api.runtime?.agent?.resolveAgentTimeoutMs?.(runtimeConfig);
30137
- const timeoutMs = Math.min(
30138
- 75e3,
30139
- typeof configuredTimeout === "number" && configuredTimeout > 0 ? configuredTimeout : 75e3
30140
- );
30703
+ const timeoutMs = typeof configuredTimeout === "number" && Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : 30 * 6e4;
30141
30704
  let selectedProvider = runtimeRoute?.provider;
30142
30705
  let selectedModel = runtimeRoute?.model;
30143
30706
  if (runtimeRoute) {
@@ -30155,7 +30718,7 @@ async function runOpenClawChatTurn(api, config, sessionKey, prompt, transcriptPr
30155
30718
  prompt,
30156
30719
  transcriptPrompt,
30157
30720
  timeoutMs,
30158
- runId: randomUUID7(),
30721
+ runId: randomUUID8(),
30159
30722
  trigger: "user",
30160
30723
  messageChannel: "perkos-chat",
30161
30724
  disableTools: true,
@@ -30198,16 +30761,13 @@ async function runOpenClawChatTurn(api, config, sessionKey, prompt, transcriptPr
30198
30761
  }
30199
30762
  const reply = extractOpenClawChatReply(result);
30200
30763
  if (!reply) {
30201
- logger.error(`[perkos-chat] embedded OpenClaw run completed without reply text for ${sessionKey}`);
30202
- return null;
30764
+ throw new Error("CHAT_RUNTIME_EMPTY_FINAL");
30203
30765
  }
30204
- logger.info(`[perkos-chat] embedded OpenClaw run completed for ${sessionKey}`);
30766
+ logger.info("[perkos-chat] embedded OpenClaw run completed");
30205
30767
  return reply;
30206
30768
  } catch (err) {
30207
- logger.error(
30208
- `[perkos-chat] embedded OpenClaw run failed for ${sessionKey}: ${err instanceof Error ? err.message : String(err)}`
30209
- );
30210
- return null;
30769
+ logger.error("[perkos-chat] CHAT_RUNTIME_RESULT_UNCERTAIN");
30770
+ throw new Error("CHAT_RUNTIME_RESULT_UNCERTAIN");
30211
30771
  }
30212
30772
  }
30213
30773
  function parseWalletFromIdentity(identity) {
@@ -30228,7 +30788,7 @@ function formatRecentChatContext(messages, currentMessageId) {
30228
30788
  const transcript = prior.join("\n");
30229
30789
  return transcript.length <= CHAT_CONTEXT_CHAR_LIMIT ? transcript : `\u2026${transcript.slice(-CHAT_CONTEXT_CHAR_LIMIT)}`;
30230
30790
  }
30231
- async function deliverToHermes(config, message, logger) {
30791
+ async function deliverToHermes2(config, message, logger) {
30232
30792
  const runtime2 = config.runtime || {};
30233
30793
  const baseUrl = (runtime2.hermesUrl || "http://127.0.0.1:8642").replace(/\/+$/, "");
30234
30794
  const endpoint = runtime2.hermesEndpoint || "/v1/responses";
@@ -30294,10 +30854,10 @@ function register(api) {
30294
30854
  );
30295
30855
  const logger = api.logger || console;
30296
30856
  const pluginVersion = loadedPluginVersion();
30297
- const pluginRoot = resolve3(dirname4(fileURLToPath3(import.meta.url)), "..");
30857
+ const pluginRoot = resolve3(dirname5(fileURLToPath3(import.meta.url)), "..");
30298
30858
  const server = new A2AServer(pluginConfig, logger, { relayOwnership: "external-required" });
30299
30859
  const lifecycleKey = a2aLifecycleKey(pluginConfig);
30300
- const lifecycleInstanceId = randomUUID7();
30860
+ const lifecycleInstanceId = randomUUID8();
30301
30861
  const pendingTasks = [];
30302
30862
  const enqueueSystemEvent = api.runtime?.system?.enqueueSystemEvent;
30303
30863
  const requestHeartbeatNow = api.runtime?.system?.requestHeartbeatNow;
@@ -30349,10 +30909,10 @@ function register(api) {
30349
30909
  });
30350
30910
  if (handledAgentically) return;
30351
30911
  if (usesHttpDelivery(runtimeKind)) {
30352
- await deliverToHermes(pluginConfig, eventText, logger);
30912
+ await deliverToHermes2(pluginConfig, eventText, logger);
30353
30913
  task.artifacts.push({
30354
30914
  kind: "artifact",
30355
- artifactId: randomUUID7(),
30915
+ artifactId: randomUUID8(),
30356
30916
  parts: [{ kind: "text", text: `debug: delivered task ${task.id} to Hermes API Server` }]
30357
30917
  });
30358
30918
  const finalText2 = await generateRuntimeReply(pluginConfig, text, marker);
@@ -30362,7 +30922,7 @@ function register(api) {
30362
30922
  if (runtimeKind === "none") {
30363
30923
  task.artifacts.push({
30364
30924
  kind: "artifact",
30365
- artifactId: randomUUID7(),
30925
+ artifactId: randomUUID8(),
30366
30926
  parts: [{ kind: "text", text: `debug: runtime delivery disabled for ${marker}` }]
30367
30927
  });
30368
30928
  return;
@@ -30375,7 +30935,7 @@ function register(api) {
30375
30935
  }
30376
30936
  task.artifacts.push({
30377
30937
  kind: "artifact",
30378
- artifactId: randomUUID7(),
30938
+ artifactId: randomUUID8(),
30379
30939
  parts: [{ kind: "text", text: `debug: enqueued system event for ${marker}` }]
30380
30940
  });
30381
30941
  if (enqueueSystemEvent) {
@@ -30459,7 +31019,7 @@ function register(api) {
30459
31019
  if (!finalText) continue;
30460
31020
  task.artifacts.push({
30461
31021
  kind: "artifact",
30462
- artifactId: randomUUID7(),
31022
+ artifactId: randomUUID8(),
30463
31023
  parts: [{ kind: "text", text: finalText }]
30464
31024
  });
30465
31025
  task.status = {
@@ -30477,21 +31037,15 @@ function register(api) {
30477
31037
  const chatEnabled = Boolean(pluginConfig.chat?.enabled && chatApiKey);
30478
31038
  const chatStore = chatEnabled ? new ChatStore({ storeRoot: pluginConfig.chat?.storeRoot }) : null;
30479
31039
  let supervisorClient;
30480
- const handleSupervisorChatDeliver = async (frame) => {
31040
+ const executeSupervisorChat = async (frame, checkpoint) => {
30481
31041
  const walletAddress = parseWalletFromIdentity(frame.from);
30482
- const maintenanceReply = await tryHandleA2AMaintenanceMarker(frame.text, pluginConfig);
31042
+ const maintenanceReply = checkpoint.runId ? null : await tryHandleA2AMaintenanceMarker(frame.text, pluginConfig);
30483
31043
  if (maintenanceReply) {
30484
- if (walletAddress) {
30485
- await supervisorClient.sendChatReply({ convId: frame.convId, walletAddress, text: maintenanceReply, replyTo: frame.id });
30486
- }
30487
- return;
31044
+ return walletAddress ? maintenanceReply : null;
30488
31045
  }
30489
- const voiceReply = await tryHandleInstalledVoiceMarker(frame.text, pluginConfig);
31046
+ const voiceReply = checkpoint.runId ? null : await tryHandleInstalledVoiceMarker(frame.text, pluginConfig);
30490
31047
  if (voiceReply) {
30491
- if (walletAddress) {
30492
- await supervisorClient.sendChatReply({ convId: frame.convId, walletAddress, text: voiceReply, replyTo: frame.id });
30493
- }
30494
- return;
31048
+ return walletAddress ? voiceReply : null;
30495
31049
  }
30496
31050
  const marker = `[PERKOS_CHAT:${frame.convId}]`;
30497
31051
  const recentPage = await chatStore?.readPage(frame.convId, {
@@ -30544,38 +31098,60 @@ function register(api) {
30544
31098
  ].filter(Boolean).join("\n");
30545
31099
  const runtimeKind = pluginConfig.runtime?.kind || "openclaw";
30546
31100
  if (usesHttpDelivery(runtimeKind)) {
30547
- try {
30548
- await deliverToHermes(pluginConfig, eventText, logger);
30549
- } catch (err) {
30550
- logger.error(`[perkos-chat] hermes delivery failed: ${err instanceof Error ? err.message : String(err)}`);
31101
+ if (autoReplyEnabled()) {
31102
+ const response = await deliverToHermes(pluginConfig, directRunPrompt, true, chatSessionKey(frame.convId), checkpoint);
31103
+ const text = extractAssistantText(response);
31104
+ if (!text) throw new Error("CHAT_RUNTIME_EMPTY_FINAL");
31105
+ return text;
30551
31106
  }
30552
- return;
31107
+ await deliverToHermes2(pluginConfig, eventText, logger);
31108
+ return null;
30553
31109
  }
30554
31110
  if (runtimeKind === "none") {
30555
31111
  logger.info(`[perkos-chat] dropped chat_deliver for ${frame.convId} (runtime=none)`);
30556
- return;
31112
+ return null;
30557
31113
  }
30558
- const chatSessionKey = openclawChatSessionKey(pluginConfig, frame.convId);
30559
- await prepareOpenClawChatSession(api, pluginConfig, chatSessionKey, logger);
31114
+ const chatSessionKey2 = openclawChatSessionKey(pluginConfig, frame.convId);
31115
+ await prepareOpenClawChatSession(api, pluginConfig, chatSessionKey2, logger);
30560
31116
  if (walletAddress) {
30561
- const embeddedReply = await runOpenClawChatTurn(api, pluginConfig, chatSessionKey, directRunPrompt, frame.text, logger);
31117
+ const embeddedReply = await runOpenClawChatTurn(api, pluginConfig, chatSessionKey2, directRunPrompt, frame.text, logger);
30562
31118
  const reply = embeddedReply ? stripPerkosChatTransportMarker(embeddedReply, frame.convId) : null;
30563
31119
  if (reply) {
30564
- const result = await supervisorClient.sendChatReply({
30565
- convId: frame.convId,
30566
- walletAddress,
30567
- text: reply,
30568
- replyTo: frame.id
30569
- });
30570
- logger.info(`[perkos-chat] embedded reply ${result.delivered ? "delivered" : "queued (offline)"} for ${frame.convId}`);
30571
- return;
31120
+ return reply;
30572
31121
  }
30573
31122
  if (embeddedReply) {
30574
- logger.error(`[perkos-chat] embedded OpenClaw reply contained only transport metadata for ${frame.convId}`);
31123
+ throw new Error("CHAT_RUNTIME_EMPTY_FINAL");
30575
31124
  }
30576
31125
  }
30577
- if (enqueueSystemEvent) enqueueSystemEvent(eventText, { sessionKey: chatSessionKey });
30578
- wakeGatewayAgent(requestHeartbeatNow, `[PerkOS-Chat] inbound message in ${frame.convId}`, logger, chatSessionKey);
31126
+ if (enqueueSystemEvent) enqueueSystemEvent(eventText, { sessionKey: chatSessionKey2 });
31127
+ wakeGatewayAgent(requestHeartbeatNow, `[PerkOS-Chat] inbound message in ${frame.convId}`, logger, chatSessionKey2);
31128
+ return null;
31129
+ };
31130
+ const chatTurns = chatStore ? new ChatTurns({
31131
+ root: chatStore.root,
31132
+ owner: pluginConfig.agentName,
31133
+ execute: executeSupervisorChat,
31134
+ error: (code) => logger.error(`[perkos-chat] ${code}`),
31135
+ progress: (frame, phase) => {
31136
+ const walletAddress = parseWalletFromIdentity(frame.from);
31137
+ if (walletAddress) void supervisorClient.sendChatProgress({ convId: frame.convId, walletAddress, replyTo: frame.id, phase }).catch(() => {
31138
+ });
31139
+ },
31140
+ reply: (turn, id) => {
31141
+ const walletAddress = parseWalletFromIdentity(turn.frame.from);
31142
+ if (!walletAddress) throw new Error("CHAT_REPLY_CONTEXT_UNAVAILABLE");
31143
+ return supervisorClient.sendChatReply({
31144
+ id,
31145
+ convId: turn.frame.convId,
31146
+ walletAddress,
31147
+ text: turn.text,
31148
+ replyTo: turn.frame.id,
31149
+ timestamp: turn.completedAt
31150
+ });
31151
+ }
31152
+ }) : null;
31153
+ const handleSupervisorChatDeliver = async (frame) => {
31154
+ if (chatTurns) await chatTurns.submit(frame);
30579
31155
  };
30580
31156
  if (pluginConfig.chat?.enabled && !chatApiKey) {
30581
31157
  logger.error("[perkos-chat] chat.enabled=true but no apiKey (set chat.apiKey or fall back from relay.apiKey)");
@@ -30622,6 +31198,7 @@ function register(api) {
30622
31198
  start: async () => {
30623
31199
  try {
30624
31200
  await supervisorClient.start();
31201
+ await chatTurns?.start();
30625
31202
  void flushMaintenanceReceipts(pluginConfig);
30626
31203
  const ownerHealth = supervisorClient.currentHealth();
30627
31204
  recordRuntimeLoadEvidence({