@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.
- package/dist/a2a-maintenance.d.ts +3 -5
- package/dist/a2a-maintenance.d.ts.map +1 -1
- package/dist/a2a-maintenance.js +37 -23
- package/dist/a2a-maintenance.js.map +1 -1
- package/dist/bridge-agent.js +49 -1
- package/dist/bridge-agent.js.map +1 -1
- package/dist/bridge-chat-reply.d.ts +4 -1
- package/dist/bridge-chat-reply.d.ts.map +1 -1
- package/dist/bridge-chat-reply.js +22 -13
- package/dist/bridge-chat-reply.js.map +1 -1
- package/dist/chat-client.d.ts +11 -4
- package/dist/chat-client.d.ts.map +1 -1
- package/dist/chat-client.js +57 -13
- package/dist/chat-client.js.map +1 -1
- package/dist/chat-outbox.d.ts +26 -0
- package/dist/chat-outbox.d.ts.map +1 -0
- package/dist/chat-outbox.js +73 -0
- package/dist/chat-outbox.js.map +1 -0
- package/dist/chat-runtime-http.d.ts +9 -0
- package/dist/chat-runtime-http.d.ts.map +1 -0
- package/dist/chat-runtime-http.js +86 -0
- package/dist/chat-runtime-http.js.map +1 -0
- package/dist/chat-store.d.ts.map +1 -1
- package/dist/chat-store.js +10 -2
- package/dist/chat-store.js.map +1 -1
- package/dist/chat-turns.d.ts +44 -0
- package/dist/chat-turns.d.ts.map +1 -0
- package/dist/chat-turns.js +146 -0
- package/dist/chat-turns.js.map +1 -0
- package/dist/chat-types.d.ts +4 -0
- package/dist/chat-types.d.ts.map +1 -1
- package/dist/hermes-connect.d.ts +2 -0
- package/dist/hermes-connect.d.ts.map +1 -1
- package/dist/hermes-connect.js +11 -0
- package/dist/hermes-connect.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +725 -148
- package/dist/index.js.map +4 -4
- package/dist/maintenance-result.d.ts +19 -0
- package/dist/maintenance-result.d.ts.map +1 -0
- package/dist/maintenance-result.js +38 -0
- package/dist/maintenance-result.js.map +1 -0
- package/dist/maintenance-runner.d.ts +15 -0
- package/dist/maintenance-runner.d.ts.map +1 -1
- package/dist/maintenance-runner.js +87 -62
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/maintenance-runner.test.d.ts +2 -0
- package/dist/maintenance-runner.test.d.ts.map +1 -0
- package/dist/maintenance-runner.test.js +153 -0
- package/dist/maintenance-runner.test.js.map +1 -0
- package/dist/openclaw-supervisor-client.d.ts +8 -0
- package/dist/openclaw-supervisor-client.d.ts.map +1 -1
- package/dist/openclaw-supervisor-client.js +3 -0
- package/dist/openclaw-supervisor-client.js.map +1 -1
- package/dist/openclaw-supervisor.d.ts.map +1 -1
- package/dist/openclaw-supervisor.js +9 -0
- package/dist/openclaw-supervisor.js.map +1 -1
- package/dist/private-chat-records.d.ts +12 -0
- package/dist/private-chat-records.d.ts.map +1 -0
- package/dist/private-chat-records.js +71 -0
- package/dist/private-chat-records.js.map +1 -0
- package/dist/service-environment.d.ts +3 -0
- package/dist/service-environment.d.ts.map +1 -0
- package/dist/service-environment.js +8 -0
- package/dist/service-environment.js.map +1 -0
- package/dist/supervisor-protocol.d.ts +1 -1
- package/dist/supervisor-protocol.d.ts.map +1 -1
- package/docs/chat-durable-delivery.md +55 -0
- package/docs/nexus-communications-server.md +1 -1
- package/docs/plans/2026-09-06-durable-chat-design.md +32 -0
- package/docs/plans/2026-09-06-managed-hermes-service-path-design.md +38 -0
- package/openclaw.plugin.json +1 -1
- 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
|
|
16884
|
+
var open3 = first;
|
|
16885
16885
|
var collected = 0;
|
|
16886
|
-
while (
|
|
16886
|
+
while (open3 >= 0 && collected < options.depth) {
|
|
16887
16887
|
var level = 1;
|
|
16888
|
-
var i =
|
|
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(
|
|
16903
|
+
segments[segments.length] = "[" + key.slice(open3) + "]";
|
|
16904
16904
|
return segments;
|
|
16905
16905
|
}
|
|
16906
|
-
var seg = key.slice(
|
|
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
|
-
|
|
16913
|
+
open3 = key.indexOf("[", close + 1);
|
|
16914
16914
|
}
|
|
16915
|
-
if (
|
|
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(
|
|
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
|
|
18444
|
+
var dirname6 = path.dirname;
|
|
18445
18445
|
var basename = path.basename;
|
|
18446
18446
|
var extname = path.extname;
|
|
18447
|
-
var
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
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 && !
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
|
27780
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
27781
27781
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
27782
|
-
import { dirname as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
28686
|
+
return createHash3("sha256").update(lifecycleKey).digest("hex");
|
|
28473
28687
|
}
|
|
28474
28688
|
function supervisorDir(stateDir) {
|
|
28475
|
-
return
|
|
28689
|
+
return join4(stateDir, "perkos-a2a", "supervisors");
|
|
28476
28690
|
}
|
|
28477
28691
|
function supervisorSocketPath(stateDir, ownerHash) {
|
|
28478
|
-
const preferred =
|
|
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
|
|
28695
|
+
return join4(tmpdir(), `perkos-a2a-${uid}`, `${ownerHash.slice(0, 32)}.sock`);
|
|
28482
28696
|
}
|
|
28483
28697
|
function supervisorBootstrapPath(stateDir, ownerHash) {
|
|
28484
|
-
return
|
|
28698
|
+
return join4(supervisorDir(stateDir), `${ownerHash}.bootstrap.json`);
|
|
28485
28699
|
}
|
|
28486
28700
|
function supervisorRolloverLockPath(stateDir, ownerHash) {
|
|
28487
|
-
return
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 ??
|
|
29261
|
+
return resolve2(stateDir ?? process.env.OPENCLAW_STATE_DIR ?? join5(homedir4(), ".openclaw"), "perkos-a2a");
|
|
29045
29262
|
}
|
|
29046
29263
|
function runtimeAttemptPath(stateDir) {
|
|
29047
|
-
return
|
|
29264
|
+
return join5(runtimeEvidenceDir(stateDir), "active-attempt.json");
|
|
29048
29265
|
}
|
|
29049
29266
|
function runtimeStatusPath(stateDir) {
|
|
29050
|
-
return
|
|
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 {
|
|
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 {
|
|
29708
|
+
import { join as join6 } from "node:path";
|
|
29316
29709
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
29317
|
-
|
|
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
|
|
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 =
|
|
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/${
|
|
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
|
|
29420
|
-
const
|
|
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 =
|
|
29867
|
+
if (!file.endsWith(".json") || !REQUEST_ID.test(file.slice(0, -5))) continue;
|
|
29868
|
+
const path = join6(receiptsDir, file);
|
|
29424
29869
|
try {
|
|
29425
|
-
const
|
|
29426
|
-
|
|
29427
|
-
await
|
|
29428
|
-
|
|
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
|
|
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
|
-
*
|
|
29540
|
-
*
|
|
29541
|
-
*
|
|
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 =
|
|
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
|
-
|
|
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
|
|
29728
|
-
|
|
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
|
-
|
|
29811
|
-
|
|
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:
|
|
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:
|
|
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 :
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
30202
|
-
return null;
|
|
30764
|
+
throw new Error("CHAT_RUNTIME_EMPTY_FINAL");
|
|
30203
30765
|
}
|
|
30204
|
-
logger.info(
|
|
30766
|
+
logger.info("[perkos-chat] embedded OpenClaw run completed");
|
|
30205
30767
|
return reply;
|
|
30206
30768
|
} catch (err) {
|
|
30207
|
-
logger.error(
|
|
30208
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
30912
|
+
await deliverToHermes2(pluginConfig, eventText, logger);
|
|
30353
30913
|
task.artifacts.push({
|
|
30354
30914
|
kind: "artifact",
|
|
30355
|
-
artifactId:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30548
|
-
await deliverToHermes(pluginConfig,
|
|
30549
|
-
|
|
30550
|
-
|
|
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
|
-
|
|
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
|
|
30559
|
-
await prepareOpenClawChatSession(api, pluginConfig,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
31123
|
+
throw new Error("CHAT_RUNTIME_EMPTY_FINAL");
|
|
30575
31124
|
}
|
|
30576
31125
|
}
|
|
30577
|
-
if (enqueueSystemEvent) enqueueSystemEvent(eventText, { sessionKey:
|
|
30578
|
-
wakeGatewayAgent(requestHeartbeatNow, `[PerkOS-Chat] inbound message in ${frame.convId}`, logger,
|
|
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({
|