@perkos/perkos-a2a 0.12.66 → 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.
- 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/index.d.ts.map +1 -1
- package/dist/index.js +634 -123
- package/dist/index.js.map +4 -4
- 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/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/openclaw.plugin.json +1 -1
- package/package.json +1 -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
|
};
|
|
@@ -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
|
|
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 =
|
|
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);
|
|
@@ -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,7 +27777,7 @@ 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
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
|
|
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,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
|
|
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
|
|
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
|
|
29721
|
+
return join6(homedir5(), ".perkos", "a2a", "hermes", agentId, "maintenance");
|
|
29329
29722
|
}
|
|
29330
29723
|
async function atomicPrivateJson(path, value) {
|
|
29331
|
-
await
|
|
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
|
|
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 =
|
|
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 =
|
|
29420
|
-
const files = await
|
|
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 =
|
|
29816
|
+
const path = join6(receiptsDir, file);
|
|
29424
29817
|
try {
|
|
29425
|
-
const receipt = JSON.parse(await
|
|
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
|
|
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
|
|
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
|
-
*
|
|
29540
|
-
*
|
|
29541
|
-
*
|
|
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 =
|
|
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
|
-
|
|
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
|
|
29728
|
-
|
|
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
|
-
|
|
29811
|
-
|
|
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:
|
|
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:
|
|
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 :
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
30202
|
-
return null;
|
|
30698
|
+
throw new Error("CHAT_RUNTIME_EMPTY_FINAL");
|
|
30203
30699
|
}
|
|
30204
|
-
logger.info(
|
|
30700
|
+
logger.info("[perkos-chat] embedded OpenClaw run completed");
|
|
30205
30701
|
return reply;
|
|
30206
30702
|
} catch (err) {
|
|
30207
|
-
logger.error(
|
|
30208
|
-
|
|
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
|
|
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 =
|
|
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
|
|
30846
|
+
await deliverToHermes2(pluginConfig, eventText, logger);
|
|
30353
30847
|
task.artifacts.push({
|
|
30354
30848
|
kind: "artifact",
|
|
30355
|
-
artifactId:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30548
|
-
await deliverToHermes(pluginConfig,
|
|
30549
|
-
|
|
30550
|
-
|
|
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
|
-
|
|
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
|
|
30559
|
-
await prepareOpenClawChatSession(api, pluginConfig,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
31057
|
+
throw new Error("CHAT_RUNTIME_EMPTY_FINAL");
|
|
30575
31058
|
}
|
|
30576
31059
|
}
|
|
30577
|
-
if (enqueueSystemEvent) enqueueSystemEvent(eventText, { sessionKey:
|
|
30578
|
-
wakeGatewayAgent(requestHeartbeatNow, `[PerkOS-Chat] inbound message in ${frame.convId}`, logger,
|
|
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({
|