@perkos/perkos-a2a 0.12.65 → 0.12.67

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 (49) hide show
  1. package/dist/bridge-agent.js +49 -1
  2. package/dist/bridge-agent.js.map +1 -1
  3. package/dist/bridge-chat-reply.d.ts +4 -1
  4. package/dist/bridge-chat-reply.d.ts.map +1 -1
  5. package/dist/bridge-chat-reply.js +22 -13
  6. package/dist/bridge-chat-reply.js.map +1 -1
  7. package/dist/chat-client.d.ts +11 -4
  8. package/dist/chat-client.d.ts.map +1 -1
  9. package/dist/chat-client.js +57 -13
  10. package/dist/chat-client.js.map +1 -1
  11. package/dist/chat-outbox.d.ts +26 -0
  12. package/dist/chat-outbox.d.ts.map +1 -0
  13. package/dist/chat-outbox.js +73 -0
  14. package/dist/chat-outbox.js.map +1 -0
  15. package/dist/chat-runtime-http.d.ts +9 -0
  16. package/dist/chat-runtime-http.d.ts.map +1 -0
  17. package/dist/chat-runtime-http.js +86 -0
  18. package/dist/chat-runtime-http.js.map +1 -0
  19. package/dist/chat-store.d.ts.map +1 -1
  20. package/dist/chat-store.js +10 -2
  21. package/dist/chat-store.js.map +1 -1
  22. package/dist/chat-turns.d.ts +44 -0
  23. package/dist/chat-turns.d.ts.map +1 -0
  24. package/dist/chat-turns.js +146 -0
  25. package/dist/chat-turns.js.map +1 -0
  26. package/dist/chat-types.d.ts +4 -0
  27. package/dist/chat-types.d.ts.map +1 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +634 -123
  30. package/dist/index.js.map +4 -4
  31. package/dist/openclaw-supervisor-client.d.ts +8 -0
  32. package/dist/openclaw-supervisor-client.d.ts.map +1 -1
  33. package/dist/openclaw-supervisor-client.js +3 -0
  34. package/dist/openclaw-supervisor-client.js.map +1 -1
  35. package/dist/openclaw-supervisor.d.ts.map +1 -1
  36. package/dist/openclaw-supervisor.js +9 -0
  37. package/dist/openclaw-supervisor.js.map +1 -1
  38. package/dist/private-chat-records.d.ts +12 -0
  39. package/dist/private-chat-records.d.ts.map +1 -0
  40. package/dist/private-chat-records.js +71 -0
  41. package/dist/private-chat-records.js.map +1 -0
  42. package/dist/supervisor-protocol.d.ts +1 -1
  43. package/dist/supervisor-protocol.d.ts.map +1 -1
  44. package/docs/chat-durable-delivery.md +55 -0
  45. package/docs/nexus-communications-server.md +1 -1
  46. package/docs/plans/2026-09-02-voice-0.1.1-resolution-design.md +20 -0
  47. package/docs/plans/2026-09-06-durable-chat-design.md +32 -0
  48. package/openclaw.plugin.json +1 -1
  49. package/package.json +2 -2
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
  };
@@ -18444,7 +18444,7 @@ var require_view = __commonJS({
18444
18444
  var dirname5 = 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) {
@@ -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);
@@ -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,7 +27777,7 @@ 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
27782
  import { dirname as dirname4, resolve as resolve3 } from "node:path";
27783
27783
  import { fileURLToPath as fileURLToPath3 } from "node:url";
@@ -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,9 +29703,9 @@ 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 { chmod, mkdir as mkdir3, readdir as readdir2, readFile as readFile3, rename as rename2, unlink as unlink2, writeFile as writeFile2 } 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 { dirname as dirname3, join as join6 } from "node:path";
29316
29709
  import { fileURLToPath as fileURLToPath2 } from "node:url";
29317
29710
  var PERKOS_A2A_UPDATE_PREFIX = "PERKOS_A2A_UPDATE:";
29318
29711
  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;
@@ -29325,15 +29718,15 @@ function parseA2AUpdateMarker(message) {
29325
29718
  }
29326
29719
  var maintenanceControlPlane = resolvePlatformControlPlane;
29327
29720
  function maintenanceRoot(agentId) {
29328
- return join4(homedir5(), ".perkos", "a2a", "hermes", agentId, "maintenance");
29721
+ return join6(homedir5(), ".perkos", "a2a", "hermes", agentId, "maintenance");
29329
29722
  }
29330
29723
  async function atomicPrivateJson(path, value) {
29331
- await mkdir2(dirname3(path), { recursive: true, mode: 448 });
29724
+ await mkdir3(dirname3(path), { recursive: true, mode: 448 });
29332
29725
  const temporary = `${path}.${process.pid}.tmp`;
29333
29726
  await writeFile2(temporary, `${JSON.stringify(value)}
29334
29727
  `, { mode: 384 });
29335
29728
  await chmod(temporary, 384);
29336
- await rename(temporary, path);
29729
+ await rename2(temporary, path);
29337
29730
  }
29338
29731
  async function authorizedJson(url, relayApiKey, init = {}) {
29339
29732
  const response = await fetch(url, {
@@ -29363,7 +29756,7 @@ async function claimRequest(target, requestId, bridgeInstanceId) {
29363
29756
  }
29364
29757
  async function queueHermesMaintenanceJob(job) {
29365
29758
  const root = maintenanceRoot(job.agentId);
29366
- const jobPath = join4(root, "jobs", `${job.requestId}.json`);
29759
+ const jobPath = join6(root, "jobs", `${job.requestId}.json`);
29367
29760
  await atomicPrivateJson(jobPath, job);
29368
29761
  const runnerPath = fileURLToPath2(new URL("./maintenance-runner.js", import.meta.url));
29369
29762
  const child = spawn2(process.execPath, [runnerPath, "--job", jobPath], {
@@ -29416,23 +29809,90 @@ async function reportReceipt(receipt, target) {
29416
29809
  async function flushMaintenanceReceipts(config) {
29417
29810
  const target = maintenanceControlPlane(config);
29418
29811
  if (!target) return;
29419
- const receiptsDir = join4(maintenanceRoot(target.agentId), "receipts");
29420
- const files = await readdir(receiptsDir).catch(() => []);
29812
+ const receiptsDir = join6(maintenanceRoot(target.agentId), "receipts");
29813
+ const files = await readdir2(receiptsDir).catch(() => []);
29421
29814
  for (const file of files) {
29422
29815
  if (!file.endsWith(".json")) continue;
29423
- const path = join4(receiptsDir, file);
29816
+ const path = join6(receiptsDir, file);
29424
29817
  try {
29425
- const receipt = JSON.parse(await readFile2(path, "utf8"));
29818
+ const receipt = JSON.parse(await readFile3(path, "utf8"));
29426
29819
  if (receipt.agentId !== target.agentId || receipt.apiBaseUrl !== target.apiBaseUrl || !REQUEST_ID.test(receipt.requestId)) continue;
29427
29820
  await reportReceipt(receipt, target);
29428
- await unlink(path);
29821
+ await unlink2(path);
29429
29822
  } catch {
29430
29823
  }
29431
29824
  }
29432
29825
  }
29433
29826
 
29434
29827
  // src/chat-client.ts
29435
- import { randomUUID as randomUUID6 } from "node:crypto";
29828
+ import { randomUUID as randomUUID7 } from "node:crypto";
29829
+
29830
+ // src/chat-outbox.ts
29831
+ import { join as join7 } from "node:path";
29832
+ var ChatOutbox = class {
29833
+ constructor(store, owner) {
29834
+ this.store = store;
29835
+ this.records = new PrivateChatRecords(join7(store.root, ".outbox", new PrivateChatRecords("").key(owner)));
29836
+ }
29837
+ store;
29838
+ records;
29839
+ pending = /* @__PURE__ */ new Map();
29840
+ nextAttempt = /* @__PURE__ */ new Map();
29841
+ attempts = /* @__PURE__ */ new Map();
29842
+ serial = Promise.resolve();
29843
+ loaded = false;
29844
+ lock(fn) {
29845
+ const work = this.serial.catch(() => {
29846
+ }).then(fn);
29847
+ this.serial = work;
29848
+ return work;
29849
+ }
29850
+ async load() {
29851
+ if (this.loaded) return;
29852
+ for (const [key, value] of await this.records.entries()) this.pending.set(key, value);
29853
+ this.loaded = true;
29854
+ }
29855
+ enqueue(entry) {
29856
+ return this.lock(async () => {
29857
+ await this.load();
29858
+ const key = this.records.key(`${entry.frame.convId}:${entry.frame.id}`);
29859
+ const existing = this.pending.get(key);
29860
+ if (existing) return;
29861
+ await this.records.put(key, entry);
29862
+ this.pending.set(key, entry);
29863
+ await this.store.appendIdempotent(entry.frame.convId, entry.message);
29864
+ });
29865
+ }
29866
+ flush(send, force = false) {
29867
+ return this.lock(async () => {
29868
+ await this.load();
29869
+ const sent = /* @__PURE__ */ new Set();
29870
+ for (const [key, entry] of this.pending) {
29871
+ if (!force && Date.now() < (this.nextAttempt.get(key) ?? 0)) continue;
29872
+ await this.store.appendIdempotent(entry.frame.convId, entry.message);
29873
+ if (!send(entry.frame)) break;
29874
+ sent.add(entry.frame.id);
29875
+ const attempt = (this.attempts.get(key) ?? 0) + 1;
29876
+ this.attempts.set(key, attempt);
29877
+ this.nextAttempt.set(key, Date.now() + Math.min(6e4, 5e3 * 2 ** Math.min(attempt - 1, 4)));
29878
+ }
29879
+ return sent;
29880
+ });
29881
+ }
29882
+ acknowledge(convId, id) {
29883
+ return this.lock(async () => {
29884
+ await this.load();
29885
+ const key = this.records.key(`${convId}:${id}`);
29886
+ if (!this.pending.has(key)) return;
29887
+ await this.records.remove(key);
29888
+ this.pending.delete(key);
29889
+ this.attempts.delete(key);
29890
+ this.nextAttempt.delete(key);
29891
+ });
29892
+ }
29893
+ };
29894
+
29895
+ // src/chat-client.ts
29436
29896
  var DEFAULT_URL = "wss://chat.perkos.xyz/chat";
29437
29897
  var DEFAULT_HISTORY_LIMIT = 50;
29438
29898
  var DEFAULT_MIN_RECONNECT = 1e3;
@@ -29449,6 +29909,9 @@ var ChatClient = class {
29449
29909
  handlers;
29450
29910
  logger;
29451
29911
  store;
29912
+ outbox;
29913
+ outboxTimer = null;
29914
+ progress = /* @__PURE__ */ new Map();
29452
29915
  reconnectMs;
29453
29916
  reconnectTimer = null;
29454
29917
  heartbeatTimer = null;
@@ -29477,6 +29940,7 @@ var ChatClient = class {
29477
29940
  this.handlers = opts.handlers ?? {};
29478
29941
  this.logger = this.handlers.logger ?? { info: console.log, error: console.error };
29479
29942
  this.store = new ChatStore({ storeRoot: this.config.storeRoot });
29943
+ this.outbox = new ChatOutbox(this.store, this.agentName);
29480
29944
  this.reconnectMs = this.config.minReconnectMs;
29481
29945
  }
29482
29946
  /** Storage handle — exposed so consumers can read history without going through the wire. */
@@ -29536,32 +30000,50 @@ var ChatClient = class {
29536
30000
  }
29537
30001
  /**
29538
30002
  * 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).
30003
+ * outbox and canonical history before sending. Offline replies are replayed
30004
+ * after authentication, with stable IDs, until acknowledged by the router.
30005
+ * `delivered` only means transmission attempted, not human receipt.
29543
30006
  */
29544
30007
  async sendReply(opts) {
29545
- const id = randomUUID6();
30008
+ const id = opts.id ?? randomUUID7();
29546
30009
  const msg = {
29547
30010
  id,
29548
30011
  from: `agent:${this.agentName}`,
29549
30012
  text: opts.text,
29550
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
30013
+ timestamp: opts.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
29551
30014
  replyTo: opts.replyTo ?? null
29552
30015
  };
29553
- await this.store.append(opts.convId, msg);
29554
30016
  const frame = {
29555
30017
  type: "chat_reply",
29556
30018
  id,
29557
30019
  convId: opts.convId,
29558
30020
  walletAddress: opts.walletAddress,
29559
30021
  text: opts.text,
29560
- replyTo: opts.replyTo ?? null
30022
+ replyTo: opts.replyTo ?? null,
30023
+ timestamp: msg.timestamp
29561
30024
  };
29562
- const delivered = this.sendFrame(frame);
30025
+ await this.outbox.enqueue({ frame, message: msg });
30026
+ const sent = await this.outbox.flush((pending) => this.authed && this.sendFrame(pending));
30027
+ const delivered = sent.has(id);
29563
30028
  return { id, delivered };
29564
30029
  }
30030
+ sendProgress(convId, walletAddress, replyTo, phase) {
30031
+ const key = `${convId}:${replyTo}`;
30032
+ const frame = {
30033
+ type: "typing",
30034
+ convId,
30035
+ walletAddress,
30036
+ replyTo,
30037
+ phase,
30038
+ state: phase === "completed" || phase === "uncertain" ? "stop" : "start"
30039
+ };
30040
+ if (phase === "completed") this.progress.delete(key);
30041
+ else this.progress.set(key, frame);
30042
+ if (this.authed) this.sendFrame(frame);
30043
+ }
30044
+ flushOutbox(force = false) {
30045
+ void this.outbox.flush((frame) => this.authed && this.sendFrame(frame), force).catch(() => this.logger.error("[perkos-chat] CHAT_OUTBOX_RETRY_PENDING"));
30046
+ }
29565
30047
  // -------------------------------------------------------------------------
29566
30048
  // WebSocket lifecycle
29567
30049
  // -------------------------------------------------------------------------
@@ -29613,7 +30095,7 @@ var ChatClient = class {
29613
30095
  clearTimeout(authTimer);
29614
30096
  authTimer = null;
29615
30097
  }
29616
- void this.handleFrame(frame);
30098
+ void this.handleFrame(frame).catch(() => this.logger.error("[perkos-chat] CHAT_FRAME_PROCESSING_FAILED"));
29617
30099
  });
29618
30100
  this.ws.on("close", (code, reason) => {
29619
30101
  if (authTimer) {
@@ -29640,6 +30122,11 @@ var ChatClient = class {
29640
30122
  this.setAuthed(true);
29641
30123
  this.logger.info(`[perkos-chat] authed as agent:${this.agentName}`);
29642
30124
  this.startHeartbeat();
30125
+ this.flushOutbox(true);
30126
+ for (const progress of this.progress.values()) this.sendFrame(progress);
30127
+ if (this.outboxTimer) clearInterval(this.outboxTimer);
30128
+ this.outboxTimer = setInterval(() => this.flushOutbox(), 5e3);
30129
+ this.outboxTimer.unref?.();
29643
30130
  return;
29644
30131
  case "auth_error":
29645
30132
  this.logger.error(`[perkos-chat] auth_error ${frame.code}: ${frame.message}`);
@@ -29674,6 +30161,8 @@ var ChatClient = class {
29674
30161
  await this.handleChannelJoin(frame);
29675
30162
  return;
29676
30163
  case "ack":
30164
+ await this.outbox.acknowledge(frame.convId, frame.id);
30165
+ return;
29677
30166
  case "pong":
29678
30167
  case "typing":
29679
30168
  return;
@@ -29724,8 +30213,8 @@ var ChatClient = class {
29724
30213
  };
29725
30214
  try {
29726
30215
  await this.store.appendIdempotent(frame.convId, msg);
29727
- } catch (err) {
29728
- this.logger.error(`[perkos-chat] failed to append to store: ${errMsg(err)}`);
30216
+ } catch {
30217
+ throw new Error("CHAT_INBOUND_PERSISTENCE_FAILED");
29729
30218
  }
29730
30219
  }
29731
30220
  async handleChannelJoin(frame) {
@@ -29804,11 +30293,18 @@ var ChatClient = class {
29804
30293
  messages: page.messages,
29805
30294
  hasMore: page.hasMore
29806
30295
  });
30296
+ for (const progress of this.progress.values()) {
30297
+ if (progress.convId === frame.convId) this.sendFrame(progress);
30298
+ }
29807
30299
  }
29808
30300
  sendFrame(frame) {
29809
30301
  if (this.ws && this.ws.readyState === wrapper_default.OPEN) {
29810
- this.ws.send(JSON.stringify(frame));
29811
- return true;
30302
+ try {
30303
+ this.ws.send(JSON.stringify(frame));
30304
+ return true;
30305
+ } catch {
30306
+ return false;
30307
+ }
29812
30308
  }
29813
30309
  return false;
29814
30310
  }
@@ -29828,6 +30324,10 @@ var ChatClient = class {
29828
30324
  this.reconnectMs = Math.min(delay * 2, this.config.maxReconnectMs);
29829
30325
  }
29830
30326
  clearTimers() {
30327
+ if (this.outboxTimer) {
30328
+ clearInterval(this.outboxTimer);
30329
+ this.outboxTimer = null;
30330
+ }
29831
30331
  if (this.heartbeatTimer) {
29832
30332
  clearInterval(this.heartbeatTimer);
29833
30333
  this.heartbeatTimer = null;
@@ -29903,7 +30403,7 @@ async function mutateOpenClawChatSession(api, config, sessionKey, update) {
29903
30403
  if (typeof patchSessionEntry === "function") {
29904
30404
  await patchSessionEntry({
29905
30405
  sessionKey,
29906
- fallbackEntry: { sessionId: randomUUID7(), updatedAt: Date.now() },
30406
+ fallbackEntry: { sessionId: randomUUID8(), updatedAt: Date.now() },
29907
30407
  preserveActivity: true,
29908
30408
  replaceEntry: true,
29909
30409
  update
@@ -29920,7 +30420,7 @@ async function mutateOpenClawChatSession(api, config, sessionKey, update) {
29920
30420
  await updateSessionStore(
29921
30421
  storePath,
29922
30422
  (store) => {
29923
- const current = store[sessionKey] ?? { sessionId: randomUUID7(), updatedAt: Date.now() };
30423
+ const current = store[sessionKey] ?? { sessionId: randomUUID8(), updatedAt: Date.now() };
29924
30424
  store[sessionKey] = update(current);
29925
30425
  return store[sessionKey];
29926
30426
  },
@@ -30132,12 +30632,9 @@ async function runOpenClawChatTurn(api, config, sessionKey, prompt, transcriptPr
30132
30632
  const workspaceDir = resolveAgentWorkspaceDir(runtimeConfig, agentId);
30133
30633
  const sessionContext = readOpenClawChatSession(api, agentId, sessionKey);
30134
30634
  const entry = sessionContext.entry;
30135
- const sessionId = typeof entry?.sessionId === "string" && entry.sessionId.trim() ? entry.sessionId : randomUUID7();
30635
+ const sessionId = typeof entry?.sessionId === "string" && entry.sessionId.trim() ? entry.sessionId : randomUUID8();
30136
30636
  const configuredTimeout = api.runtime?.agent?.resolveAgentTimeoutMs?.(runtimeConfig);
30137
- const timeoutMs = Math.min(
30138
- 75e3,
30139
- typeof configuredTimeout === "number" && configuredTimeout > 0 ? configuredTimeout : 75e3
30140
- );
30637
+ const timeoutMs = typeof configuredTimeout === "number" && Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : 30 * 6e4;
30141
30638
  let selectedProvider = runtimeRoute?.provider;
30142
30639
  let selectedModel = runtimeRoute?.model;
30143
30640
  if (runtimeRoute) {
@@ -30155,7 +30652,7 @@ async function runOpenClawChatTurn(api, config, sessionKey, prompt, transcriptPr
30155
30652
  prompt,
30156
30653
  transcriptPrompt,
30157
30654
  timeoutMs,
30158
- runId: randomUUID7(),
30655
+ runId: randomUUID8(),
30159
30656
  trigger: "user",
30160
30657
  messageChannel: "perkos-chat",
30161
30658
  disableTools: true,
@@ -30198,16 +30695,13 @@ async function runOpenClawChatTurn(api, config, sessionKey, prompt, transcriptPr
30198
30695
  }
30199
30696
  const reply = extractOpenClawChatReply(result);
30200
30697
  if (!reply) {
30201
- logger.error(`[perkos-chat] embedded OpenClaw run completed without reply text for ${sessionKey}`);
30202
- return null;
30698
+ throw new Error("CHAT_RUNTIME_EMPTY_FINAL");
30203
30699
  }
30204
- logger.info(`[perkos-chat] embedded OpenClaw run completed for ${sessionKey}`);
30700
+ logger.info("[perkos-chat] embedded OpenClaw run completed");
30205
30701
  return reply;
30206
30702
  } 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;
30703
+ logger.error("[perkos-chat] CHAT_RUNTIME_RESULT_UNCERTAIN");
30704
+ throw new Error("CHAT_RUNTIME_RESULT_UNCERTAIN");
30211
30705
  }
30212
30706
  }
30213
30707
  function parseWalletFromIdentity(identity) {
@@ -30228,7 +30722,7 @@ function formatRecentChatContext(messages, currentMessageId) {
30228
30722
  const transcript = prior.join("\n");
30229
30723
  return transcript.length <= CHAT_CONTEXT_CHAR_LIMIT ? transcript : `\u2026${transcript.slice(-CHAT_CONTEXT_CHAR_LIMIT)}`;
30230
30724
  }
30231
- async function deliverToHermes(config, message, logger) {
30725
+ async function deliverToHermes2(config, message, logger) {
30232
30726
  const runtime2 = config.runtime || {};
30233
30727
  const baseUrl = (runtime2.hermesUrl || "http://127.0.0.1:8642").replace(/\/+$/, "");
30234
30728
  const endpoint = runtime2.hermesEndpoint || "/v1/responses";
@@ -30297,7 +30791,7 @@ function register(api) {
30297
30791
  const pluginRoot = resolve3(dirname4(fileURLToPath3(import.meta.url)), "..");
30298
30792
  const server = new A2AServer(pluginConfig, logger, { relayOwnership: "external-required" });
30299
30793
  const lifecycleKey = a2aLifecycleKey(pluginConfig);
30300
- const lifecycleInstanceId = randomUUID7();
30794
+ const lifecycleInstanceId = randomUUID8();
30301
30795
  const pendingTasks = [];
30302
30796
  const enqueueSystemEvent = api.runtime?.system?.enqueueSystemEvent;
30303
30797
  const requestHeartbeatNow = api.runtime?.system?.requestHeartbeatNow;
@@ -30349,10 +30843,10 @@ function register(api) {
30349
30843
  });
30350
30844
  if (handledAgentically) return;
30351
30845
  if (usesHttpDelivery(runtimeKind)) {
30352
- await deliverToHermes(pluginConfig, eventText, logger);
30846
+ await deliverToHermes2(pluginConfig, eventText, logger);
30353
30847
  task.artifacts.push({
30354
30848
  kind: "artifact",
30355
- artifactId: randomUUID7(),
30849
+ artifactId: randomUUID8(),
30356
30850
  parts: [{ kind: "text", text: `debug: delivered task ${task.id} to Hermes API Server` }]
30357
30851
  });
30358
30852
  const finalText2 = await generateRuntimeReply(pluginConfig, text, marker);
@@ -30362,7 +30856,7 @@ function register(api) {
30362
30856
  if (runtimeKind === "none") {
30363
30857
  task.artifacts.push({
30364
30858
  kind: "artifact",
30365
- artifactId: randomUUID7(),
30859
+ artifactId: randomUUID8(),
30366
30860
  parts: [{ kind: "text", text: `debug: runtime delivery disabled for ${marker}` }]
30367
30861
  });
30368
30862
  return;
@@ -30375,7 +30869,7 @@ function register(api) {
30375
30869
  }
30376
30870
  task.artifacts.push({
30377
30871
  kind: "artifact",
30378
- artifactId: randomUUID7(),
30872
+ artifactId: randomUUID8(),
30379
30873
  parts: [{ kind: "text", text: `debug: enqueued system event for ${marker}` }]
30380
30874
  });
30381
30875
  if (enqueueSystemEvent) {
@@ -30459,7 +30953,7 @@ function register(api) {
30459
30953
  if (!finalText) continue;
30460
30954
  task.artifacts.push({
30461
30955
  kind: "artifact",
30462
- artifactId: randomUUID7(),
30956
+ artifactId: randomUUID8(),
30463
30957
  parts: [{ kind: "text", text: finalText }]
30464
30958
  });
30465
30959
  task.status = {
@@ -30477,21 +30971,15 @@ function register(api) {
30477
30971
  const chatEnabled = Boolean(pluginConfig.chat?.enabled && chatApiKey);
30478
30972
  const chatStore = chatEnabled ? new ChatStore({ storeRoot: pluginConfig.chat?.storeRoot }) : null;
30479
30973
  let supervisorClient;
30480
- const handleSupervisorChatDeliver = async (frame) => {
30974
+ const executeSupervisorChat = async (frame, checkpoint) => {
30481
30975
  const walletAddress = parseWalletFromIdentity(frame.from);
30482
- const maintenanceReply = await tryHandleA2AMaintenanceMarker(frame.text, pluginConfig);
30976
+ const maintenanceReply = checkpoint.runId ? null : await tryHandleA2AMaintenanceMarker(frame.text, pluginConfig);
30483
30977
  if (maintenanceReply) {
30484
- if (walletAddress) {
30485
- await supervisorClient.sendChatReply({ convId: frame.convId, walletAddress, text: maintenanceReply, replyTo: frame.id });
30486
- }
30487
- return;
30978
+ return walletAddress ? maintenanceReply : null;
30488
30979
  }
30489
- const voiceReply = await tryHandleInstalledVoiceMarker(frame.text, pluginConfig);
30980
+ const voiceReply = checkpoint.runId ? null : await tryHandleInstalledVoiceMarker(frame.text, pluginConfig);
30490
30981
  if (voiceReply) {
30491
- if (walletAddress) {
30492
- await supervisorClient.sendChatReply({ convId: frame.convId, walletAddress, text: voiceReply, replyTo: frame.id });
30493
- }
30494
- return;
30982
+ return walletAddress ? voiceReply : null;
30495
30983
  }
30496
30984
  const marker = `[PERKOS_CHAT:${frame.convId}]`;
30497
30985
  const recentPage = await chatStore?.readPage(frame.convId, {
@@ -30544,38 +31032,60 @@ function register(api) {
30544
31032
  ].filter(Boolean).join("\n");
30545
31033
  const runtimeKind = pluginConfig.runtime?.kind || "openclaw";
30546
31034
  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)}`);
31035
+ if (autoReplyEnabled()) {
31036
+ const response = await deliverToHermes(pluginConfig, directRunPrompt, true, chatSessionKey(frame.convId), checkpoint);
31037
+ const text = extractAssistantText(response);
31038
+ if (!text) throw new Error("CHAT_RUNTIME_EMPTY_FINAL");
31039
+ return text;
30551
31040
  }
30552
- return;
31041
+ await deliverToHermes2(pluginConfig, eventText, logger);
31042
+ return null;
30553
31043
  }
30554
31044
  if (runtimeKind === "none") {
30555
31045
  logger.info(`[perkos-chat] dropped chat_deliver for ${frame.convId} (runtime=none)`);
30556
- return;
31046
+ return null;
30557
31047
  }
30558
- const chatSessionKey = openclawChatSessionKey(pluginConfig, frame.convId);
30559
- await prepareOpenClawChatSession(api, pluginConfig, chatSessionKey, logger);
31048
+ const chatSessionKey2 = openclawChatSessionKey(pluginConfig, frame.convId);
31049
+ await prepareOpenClawChatSession(api, pluginConfig, chatSessionKey2, logger);
30560
31050
  if (walletAddress) {
30561
- const embeddedReply = await runOpenClawChatTurn(api, pluginConfig, chatSessionKey, directRunPrompt, frame.text, logger);
31051
+ const embeddedReply = await runOpenClawChatTurn(api, pluginConfig, chatSessionKey2, directRunPrompt, frame.text, logger);
30562
31052
  const reply = embeddedReply ? stripPerkosChatTransportMarker(embeddedReply, frame.convId) : null;
30563
31053
  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;
31054
+ return reply;
30572
31055
  }
30573
31056
  if (embeddedReply) {
30574
- logger.error(`[perkos-chat] embedded OpenClaw reply contained only transport metadata for ${frame.convId}`);
31057
+ throw new Error("CHAT_RUNTIME_EMPTY_FINAL");
30575
31058
  }
30576
31059
  }
30577
- if (enqueueSystemEvent) enqueueSystemEvent(eventText, { sessionKey: chatSessionKey });
30578
- wakeGatewayAgent(requestHeartbeatNow, `[PerkOS-Chat] inbound message in ${frame.convId}`, logger, chatSessionKey);
31060
+ if (enqueueSystemEvent) enqueueSystemEvent(eventText, { sessionKey: chatSessionKey2 });
31061
+ wakeGatewayAgent(requestHeartbeatNow, `[PerkOS-Chat] inbound message in ${frame.convId}`, logger, chatSessionKey2);
31062
+ return null;
31063
+ };
31064
+ const chatTurns = chatStore ? new ChatTurns({
31065
+ root: chatStore.root,
31066
+ owner: pluginConfig.agentName,
31067
+ execute: executeSupervisorChat,
31068
+ error: (code) => logger.error(`[perkos-chat] ${code}`),
31069
+ progress: (frame, phase) => {
31070
+ const walletAddress = parseWalletFromIdentity(frame.from);
31071
+ if (walletAddress) void supervisorClient.sendChatProgress({ convId: frame.convId, walletAddress, replyTo: frame.id, phase }).catch(() => {
31072
+ });
31073
+ },
31074
+ reply: (turn, id) => {
31075
+ const walletAddress = parseWalletFromIdentity(turn.frame.from);
31076
+ if (!walletAddress) throw new Error("CHAT_REPLY_CONTEXT_UNAVAILABLE");
31077
+ return supervisorClient.sendChatReply({
31078
+ id,
31079
+ convId: turn.frame.convId,
31080
+ walletAddress,
31081
+ text: turn.text,
31082
+ replyTo: turn.frame.id,
31083
+ timestamp: turn.completedAt
31084
+ });
31085
+ }
31086
+ }) : null;
31087
+ const handleSupervisorChatDeliver = async (frame) => {
31088
+ if (chatTurns) await chatTurns.submit(frame);
30579
31089
  };
30580
31090
  if (pluginConfig.chat?.enabled && !chatApiKey) {
30581
31091
  logger.error("[perkos-chat] chat.enabled=true but no apiKey (set chat.apiKey or fall back from relay.apiKey)");
@@ -30622,6 +31132,7 @@ function register(api) {
30622
31132
  start: async () => {
30623
31133
  try {
30624
31134
  await supervisorClient.start();
31135
+ await chatTurns?.start();
30625
31136
  void flushMaintenanceReceipts(pluginConfig);
30626
31137
  const ownerHealth = supervisorClient.currentHealth();
30627
31138
  recordRuntimeLoadEvidence({