@agentvault/grok-bridge 0.4.12 → 0.4.13
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.d.ts +17 -0
- package/dist/index.js +480 -140
- package/package.json +1 -1
package/dist/bridge.d.ts
CHANGED
|
@@ -163,6 +163,23 @@ export declare function attachLifecycle(channel: LifecycleChannel, opts?: {
|
|
|
163
163
|
* unmarked turn already reads as a DM, which is what the model assumes.
|
|
164
164
|
*/
|
|
165
165
|
export declare function roomTurnPrefix(roomId: string, senderName: string, roomName?: string): string;
|
|
166
|
+
/**
|
|
167
|
+
* Does this agent owe a reply to this room message? (#1107)
|
|
168
|
+
*
|
|
169
|
+
* This bridge used to answer with `e.senderIsAgent === false` — "reply to any
|
|
170
|
+
* human". That was a THIRD implementation of the room reply rule, alongside the
|
|
171
|
+
* OpenClaw plugin and hermes, and #1107 shipped to the other two only. Under the
|
|
172
|
+
* old rule this agent would still have answered every un-mentioned message while
|
|
173
|
+
* wren, cortina, smartcode and smartcoder2 stayed quiet: the cascade, half-fixed,
|
|
174
|
+
* in the half most likely to be read as "the protocol doesn't work".
|
|
175
|
+
*
|
|
176
|
+
* Now it defers to the ONE shared implementation exported by
|
|
177
|
+
* `@agentvault/agentvault`. The message is still pushed into the session either
|
|
178
|
+
* way — the agent reads the whole room and only the reply obligation changes.
|
|
179
|
+
*/
|
|
180
|
+
export declare function _makeOwesReply(agentName: string | undefined, channel: {
|
|
181
|
+
ownDisplayName?: string;
|
|
182
|
+
}, log: (msg: string) => void): (e: RoomMessage) => boolean;
|
|
166
183
|
export declare function wireBridge(channel: RoomChannel, session: RoomSession, target: ActiveTarget, opts?: {
|
|
167
184
|
/**
|
|
168
185
|
* #1107: this agent's own name, used to recognise its own @mention. Threaded
|
package/dist/index.js
CHANGED
|
@@ -1115,6 +1115,58 @@ var init_mls_commit_classify = __esm2({
|
|
|
1115
1115
|
"use strict";
|
|
1116
1116
|
}
|
|
1117
1117
|
});
|
|
1118
|
+
function parseMentions(text) {
|
|
1119
|
+
const mentions = [];
|
|
1120
|
+
const re2 = new RegExp(MENTION_RE.source, MENTION_RE.flags);
|
|
1121
|
+
let match;
|
|
1122
|
+
while ((match = re2.exec(text)) !== null) {
|
|
1123
|
+
mentions.push(match[1].toLowerCase());
|
|
1124
|
+
}
|
|
1125
|
+
return mentions;
|
|
1126
|
+
}
|
|
1127
|
+
function resolveOwn(mention, names) {
|
|
1128
|
+
if (names.has(mention)) return mention;
|
|
1129
|
+
const parts = mention.split("-");
|
|
1130
|
+
for (let i2 = parts.length - 1; i2 > 0; i2--) {
|
|
1131
|
+
const candidate = parts.slice(0, i2).join("-");
|
|
1132
|
+
if (names.has(candidate)) return candidate;
|
|
1133
|
+
}
|
|
1134
|
+
return null;
|
|
1135
|
+
}
|
|
1136
|
+
function owesRoomReply(plaintext, agentName, accountId, senderIsAgent, roomDisplayName, ownDisplayName, selfResolved = true) {
|
|
1137
|
+
const mentions = parseMentions(plaintext);
|
|
1138
|
+
if (mentions.length === 0) return false;
|
|
1139
|
+
const ownNames = ownRoomNames(agentName, accountId, roomDisplayName, ownDisplayName);
|
|
1140
|
+
if (mentions.some((m2) => resolveOwn(m2, ownNames) !== null)) return true;
|
|
1141
|
+
if (mentions.some((m2) => resolveOwn(m2, BROADCAST_NAMES) !== null)) {
|
|
1142
|
+
return senderIsAgent !== true;
|
|
1143
|
+
}
|
|
1144
|
+
if (!selfResolved) return true;
|
|
1145
|
+
return false;
|
|
1146
|
+
}
|
|
1147
|
+
function ownRoomNames(agentName, accountId, roomDisplayName, ownDisplayName) {
|
|
1148
|
+
const names = /* @__PURE__ */ new Set();
|
|
1149
|
+
const add4 = (n2) => {
|
|
1150
|
+
if (!n2) return;
|
|
1151
|
+
const lower = n2.toLowerCase();
|
|
1152
|
+
names.add(lower);
|
|
1153
|
+
names.add(lower.split(/[\s(]/)[0]);
|
|
1154
|
+
};
|
|
1155
|
+
add4(agentName);
|
|
1156
|
+
add4(accountId);
|
|
1157
|
+
add4(roomDisplayName);
|
|
1158
|
+
add4(ownDisplayName);
|
|
1159
|
+
return names;
|
|
1160
|
+
}
|
|
1161
|
+
var MENTION_RE;
|
|
1162
|
+
var BROADCAST_NAMES;
|
|
1163
|
+
var init_room_protocol = __esm2({
|
|
1164
|
+
"src/room-protocol.ts"() {
|
|
1165
|
+
"use strict";
|
|
1166
|
+
MENTION_RE = /@([\w-]+)/gi;
|
|
1167
|
+
BROADCAST_NAMES = /* @__PURE__ */ new Set(["all", "everyone"]);
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1118
1170
|
var __filename;
|
|
1119
1171
|
var __dirname;
|
|
1120
1172
|
var url2;
|
|
@@ -64017,10 +64069,10 @@ var init_mls_group = __esm2({
|
|
|
64017
64069
|
const commits = [];
|
|
64018
64070
|
if (leavesToRemove.length > 0) {
|
|
64019
64071
|
const { commit } = await this.removeMembers(leavesToRemove);
|
|
64020
|
-
commits.push({ commit, epoch: this.epoch });
|
|
64072
|
+
commits.push({ commit, epoch: this.epoch, state: this.exportState() });
|
|
64021
64073
|
}
|
|
64022
64074
|
const added = await this.addMembers(toAdd);
|
|
64023
|
-
commits.push({ commit: added.commit, epoch: this.epoch });
|
|
64075
|
+
commits.push({ commit: added.commit, epoch: this.epoch, state: this.exportState() });
|
|
64024
64076
|
return { commits, welcome: added.welcome };
|
|
64025
64077
|
}
|
|
64026
64078
|
async removeMembers(memberIndices) {
|
|
@@ -64605,11 +64657,11 @@ var init_mls_kp_pool = __esm2({
|
|
|
64605
64657
|
}
|
|
64606
64658
|
});
|
|
64607
64659
|
function ownIdentity() {
|
|
64608
|
-
const v2 = true ? "0.23.
|
|
64660
|
+
const v2 = true ? "0.23.40" : FALLBACK;
|
|
64609
64661
|
return `${PACKAGE}@${v2}`;
|
|
64610
64662
|
}
|
|
64611
64663
|
function buildSha() {
|
|
64612
|
-
const raw = true ? "
|
|
64664
|
+
const raw = true ? "dbac15d-dirty" : "";
|
|
64613
64665
|
const cleaned = (raw ?? "").trim();
|
|
64614
64666
|
return !cleaned || cleaned === "unknown" ? null : cleaned;
|
|
64615
64667
|
}
|
|
@@ -65531,6 +65583,7 @@ var init_channel = __esm2({
|
|
|
65531
65583
|
"use strict";
|
|
65532
65584
|
init_abstention();
|
|
65533
65585
|
init_mls_commit_classify();
|
|
65586
|
+
init_room_protocol();
|
|
65534
65587
|
await init_libsodium_wrappers();
|
|
65535
65588
|
await init_dist();
|
|
65536
65589
|
await init_dist();
|
|
@@ -65733,6 +65786,35 @@ var init_channel = __esm2({
|
|
|
65733
65786
|
|
|
65734
65787
|
${text}`;
|
|
65735
65788
|
}
|
|
65789
|
+
/**
|
|
65790
|
+
* #1161: room attachments whose caption addressed nobody, NOT yet fetched.
|
|
65791
|
+
* The reply gate lives in every consumer of `room_message` (openclaw-entry
|
|
65792
|
+
* and the three native bridges), but its first clause is shared and needs no
|
|
65793
|
+
* identity: a caption with no mention token owes nobody a reply (#1107), so
|
|
65794
|
+
* nobody needs the file yet. Kept per room, newest last, and fetched on the
|
|
65795
|
+
* room's next ADDRESSED turn — the owner attaches, then asks (#1159/#1181).
|
|
65796
|
+
* Hermes does the same (`_defer_room_attachment`).
|
|
65797
|
+
*/
|
|
65798
|
+
_deferredRoomAttachments = /* @__PURE__ */ new Map();
|
|
65799
|
+
static DEFERRED_ROOM_ATTACHMENT_MAX = 5;
|
|
65800
|
+
static DEFERRED_ROOM_ATTACHMENT_MS = 60 * 60 * 1e3;
|
|
65801
|
+
_deferRoomAttachment(roomId, info) {
|
|
65802
|
+
const list = this._deferredRoomAttachments.get(roomId) ?? [];
|
|
65803
|
+
list.push({ info, at: Date.now() });
|
|
65804
|
+
while (list.length > _SecureChannel.DEFERRED_ROOM_ATTACHMENT_MAX) list.shift();
|
|
65805
|
+
this._deferredRoomAttachments.set(roomId, list);
|
|
65806
|
+
console.log(
|
|
65807
|
+
`[SecureChannel] room ${roomId.slice(0, 8)}: attachment ${String(info?.filename ?? "file")} deferred \u2014 caption addresses nobody, fetch on the next @-addressed turn (#1161)`
|
|
65808
|
+
);
|
|
65809
|
+
}
|
|
65810
|
+
/** Drain the room's deferred attachments that are still fresh (oldest first). */
|
|
65811
|
+
_takeDeferredRoomAttachments(roomId) {
|
|
65812
|
+
const list = this._deferredRoomAttachments.get(roomId);
|
|
65813
|
+
if (!list?.length) return [];
|
|
65814
|
+
this._deferredRoomAttachments.delete(roomId);
|
|
65815
|
+
const cutoff = Date.now() - _SecureChannel.DEFERRED_ROOM_ATTACHMENT_MS;
|
|
65816
|
+
return list.filter((d2) => d2.at >= cutoff).map((d2) => d2.info);
|
|
65817
|
+
}
|
|
65736
65818
|
/** In-flight roster-refresh promises (per room) — dedup concurrent refreshes. */
|
|
65737
65819
|
_roomRosterRefreshInFlight = /* @__PURE__ */ new Map();
|
|
65738
65820
|
/** Dedup buffer for A2A message IDs (prevents double-delivery via direct + Redis) */
|
|
@@ -66828,7 +66910,7 @@ ${text}`;
|
|
|
66828
66910
|
*/
|
|
66829
66911
|
sendActivitySpan(spanData) {
|
|
66830
66912
|
if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;
|
|
66831
|
-
const pluginVersion = true ? "0.23.
|
|
66913
|
+
const pluginVersion = true ? "0.23.40" : "0.0.0-dev";
|
|
66832
66914
|
const agentName = this.config.agentName ?? "Agent";
|
|
66833
66915
|
const resource = {
|
|
66834
66916
|
"service.name": "agentvault-agent",
|
|
@@ -68766,7 +68848,7 @@ ${text}`;
|
|
|
68766
68848
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
68767
68849
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
68768
68850
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
68769
|
-
pluginVersion: true ? "0.23.
|
|
68851
|
+
pluginVersion: true ? "0.23.40" : "0.0.0-dev"
|
|
68770
68852
|
});
|
|
68771
68853
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
68772
68854
|
}
|
|
@@ -69090,7 +69172,7 @@ ${text}`;
|
|
|
69090
69172
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
69091
69173
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
69092
69174
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
69093
|
-
pluginVersion: true ? "0.23.
|
|
69175
|
+
pluginVersion: true ? "0.23.40" : "0.0.0-dev"
|
|
69094
69176
|
});
|
|
69095
69177
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
69096
69178
|
}
|
|
@@ -69404,6 +69486,7 @@ ${text}`;
|
|
|
69404
69486
|
`[deliver] delivery_result=REJECTED group=${rejectedGroup || "unknown"} detail=${detail}`
|
|
69405
69487
|
);
|
|
69406
69488
|
console.error(`[SecureChannel] Server error: ${detail}`);
|
|
69489
|
+
this._noteCommitRefusal(data.data || data);
|
|
69407
69490
|
await this._handleServerError(data.data || data);
|
|
69408
69491
|
this.emit("error", new Error(`Server: ${detail}`));
|
|
69409
69492
|
}
|
|
@@ -69705,7 +69788,7 @@ ${text}`;
|
|
|
69705
69788
|
try {
|
|
69706
69789
|
const parsed = JSON.parse(plaintext);
|
|
69707
69790
|
messageType = parsed.type || "message";
|
|
69708
|
-
messageText = parsed.text
|
|
69791
|
+
messageText = parsed.text ?? plaintext;
|
|
69709
69792
|
if (parsed.attachment) {
|
|
69710
69793
|
attachmentInfo = parsed.attachment;
|
|
69711
69794
|
}
|
|
@@ -70457,7 +70540,7 @@ ${baseText}`;
|
|
|
70457
70540
|
try {
|
|
70458
70541
|
const parsed = JSON.parse(rawPlaintext);
|
|
70459
70542
|
messageType = parsed.type || "message";
|
|
70460
|
-
messageText = parsed.text
|
|
70543
|
+
messageText = parsed.text ?? rawPlaintext;
|
|
70461
70544
|
if (parsed.attachment) roomAttachment = parsed.attachment;
|
|
70462
70545
|
} catch {
|
|
70463
70546
|
messageType = "message";
|
|
@@ -70498,10 +70581,19 @@ ${baseText}`;
|
|
|
70498
70581
|
await this._refreshRoomRoster(resolvedRoomId);
|
|
70499
70582
|
}
|
|
70500
70583
|
}
|
|
70501
|
-
|
|
70502
|
-
|
|
70584
|
+
const addressed = parseMentions(messageText).length > 0;
|
|
70585
|
+
if (roomAttachment && !addressed) {
|
|
70586
|
+
this._deferRoomAttachment(resolvedRoomId, roomAttachment);
|
|
70503
70587
|
} else {
|
|
70504
|
-
|
|
70588
|
+
const deferred = addressed ? this._takeDeferredRoomAttachments(resolvedRoomId) : [];
|
|
70589
|
+
for (const info of deferred) {
|
|
70590
|
+
messageText = await this._augmentWithAttachment(info, messageText, resolvedRoomId);
|
|
70591
|
+
}
|
|
70592
|
+
if (roomAttachment) {
|
|
70593
|
+
messageText = await this._augmentWithAttachment(roomAttachment, messageText, resolvedRoomId);
|
|
70594
|
+
} else if (deferred.length === 0) {
|
|
70595
|
+
messageText = this._withRecentRoomAttachmentNote(resolvedRoomId, messageText);
|
|
70596
|
+
}
|
|
70505
70597
|
}
|
|
70506
70598
|
const roomMembers = this._persisted?.rooms?.[resolvedRoomId]?.members ?? [];
|
|
70507
70599
|
const senderMember = roomMembers.find((m2) => m2.deviceId === senderDeviceId);
|
|
@@ -70625,6 +70717,12 @@ ${baseText}`;
|
|
|
70625
70717
|
const groupId = data.group_id;
|
|
70626
70718
|
const epoch = typeof data.epoch === "number" ? data.epoch : 0;
|
|
70627
70719
|
this._noteGroupEpoch(groupId, data.epoch);
|
|
70720
|
+
const held = this._commitInFlight.get(groupId);
|
|
70721
|
+
if (held) {
|
|
70722
|
+
held.frames.push(data);
|
|
70723
|
+
console.log(`[SecureChannel] MLS commit for ${groupId.slice(0, 8)} held (epoch=${epoch}) \u2014 our own commit is awaiting confirmation (#1184)`);
|
|
70724
|
+
return true;
|
|
70725
|
+
}
|
|
70628
70726
|
const target = this._resolveGroupFamily(groupId);
|
|
70629
70727
|
if (target) {
|
|
70630
70728
|
const { managerKey: groupKey, label } = target;
|
|
@@ -70867,73 +70965,146 @@ ${baseText}`;
|
|
|
70867
70965
|
return 0;
|
|
70868
70966
|
}
|
|
70869
70967
|
const snapshot = mlsGroup.exportState();
|
|
70870
|
-
|
|
70871
|
-
|
|
70872
|
-
let sentCommits = 0;
|
|
70873
|
-
for (const c2 of commits) {
|
|
70874
|
-
const ok = await this._sendFrameConfirmed(
|
|
70875
|
-
{
|
|
70876
|
-
event: "mls_commit",
|
|
70877
|
-
data: {
|
|
70878
|
-
group_id: mlsGroupId,
|
|
70879
|
-
// The epoch this commit PRODUCED. After two commits the manager
|
|
70880
|
-
// only knows the last one, which is why the result carries them.
|
|
70881
|
-
epoch: Number(c2.epoch),
|
|
70882
|
-
payload: Buffer.from(c2.commit).toString("hex"),
|
|
70883
|
-
...parent2 ? { parent_digest: parent2 } : {}
|
|
70884
|
-
}
|
|
70885
|
-
},
|
|
70886
|
-
`${label} commit ${sentCommits + 1}/${commits.length}`
|
|
70887
|
-
);
|
|
70888
|
-
if (!ok) break;
|
|
70889
|
-
parent2 = this._commitDigest(c2.commit);
|
|
70890
|
-
sentCommits++;
|
|
70891
|
-
}
|
|
70892
|
-
if (sentCommits === 0) {
|
|
70893
|
-
mlsGroup.importState(snapshot);
|
|
70894
|
-
console.warn(
|
|
70895
|
-
`[SecureChannel] ${label}: commit was NOT written \u2014 rolled back to epoch ${Number(mlsGroup.epoch)}, nothing fulfilled, requests stay pending`
|
|
70896
|
-
);
|
|
70897
|
-
return 0;
|
|
70898
|
-
}
|
|
70899
|
-
await saveMlsState(this.config.dataDir, mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
70900
|
-
if (sentCommits < commits.length) {
|
|
70901
|
-
console.warn(
|
|
70902
|
-
`[SecureChannel] ${label}: only ${sentCommits}/${commits.length} commits reached the wire \u2014 requests stay PENDING for the next pull`
|
|
70903
|
-
);
|
|
70904
|
-
return 0;
|
|
70905
|
-
}
|
|
70906
|
-
if (!welcome) {
|
|
70968
|
+
const preHead = await this._readGroupHead(mlsGroupId);
|
|
70969
|
+
if (preHead.ok && preHead.epoch !== Number(mlsGroup.epoch)) {
|
|
70907
70970
|
console.warn(
|
|
70908
|
-
`[SecureChannel] ${label}:
|
|
70971
|
+
`[SecureChannel] ${label}: server head is at epoch ${preHead.epoch}, we are at ${Number(mlsGroup.epoch)} \u2014 NOT committing (it would be refused, #1184); ${targets.length} request(s) stay pending`
|
|
70909
70972
|
);
|
|
70910
70973
|
return 0;
|
|
70911
70974
|
}
|
|
70912
|
-
let
|
|
70913
|
-
|
|
70914
|
-
|
|
70915
|
-
|
|
70916
|
-
|
|
70917
|
-
|
|
70918
|
-
|
|
70919
|
-
|
|
70920
|
-
|
|
70921
|
-
|
|
70922
|
-
|
|
70923
|
-
|
|
70924
|
-
|
|
70925
|
-
|
|
70926
|
-
|
|
70975
|
+
let parent2 = preHead.ok ? preHead.digest : void 0;
|
|
70976
|
+
const inFlight = { frames: [], sent: [] };
|
|
70977
|
+
this._commitInFlight.set(mlsGroupId, inFlight);
|
|
70978
|
+
try {
|
|
70979
|
+
const { commits, welcome } = await mlsGroup.reAdmitMembers(targets.map((t2) => t2.keyPackage));
|
|
70980
|
+
let sentCommits = 0;
|
|
70981
|
+
let refusedAt = -1;
|
|
70982
|
+
let verdict = "confirmed";
|
|
70983
|
+
for (const c2 of commits) {
|
|
70984
|
+
const ours = this._commitDigest(c2.commit);
|
|
70985
|
+
inFlight.sent.push({
|
|
70986
|
+
epoch: Number(c2.epoch),
|
|
70987
|
+
digest: ours,
|
|
70988
|
+
verified: false,
|
|
70989
|
+
stateBefore: sentCommits === 0 ? snapshot : commits[sentCommits - 1].state
|
|
70990
|
+
});
|
|
70991
|
+
const ok = await this._sendFrameConfirmed(
|
|
70992
|
+
{
|
|
70993
|
+
event: "mls_commit",
|
|
70994
|
+
data: {
|
|
70995
|
+
group_id: mlsGroupId,
|
|
70996
|
+
// The epoch this commit PRODUCED. After two commits the manager
|
|
70997
|
+
// only knows the last one, which is why the result carries them.
|
|
70998
|
+
epoch: Number(c2.epoch),
|
|
70999
|
+
payload: Buffer.from(c2.commit).toString("hex"),
|
|
71000
|
+
...parent2 ? { parent_digest: parent2 } : {}
|
|
71001
|
+
}
|
|
71002
|
+
},
|
|
71003
|
+
`${label} commit ${sentCommits + 1}/${commits.length}`
|
|
71004
|
+
);
|
|
71005
|
+
if (!ok) {
|
|
71006
|
+
verdict = "unsent";
|
|
71007
|
+
inFlight.sent.pop();
|
|
71008
|
+
break;
|
|
71009
|
+
}
|
|
71010
|
+
verdict = await this._confirmCommitAtHead(mlsGroupId, Number(c2.epoch), ours, inFlight, label);
|
|
71011
|
+
if (verdict === "confirmed") {
|
|
71012
|
+
inFlight.sent[sentCommits].verified = true;
|
|
71013
|
+
} else if (verdict === "unverifiable") {
|
|
71014
|
+
console.warn(
|
|
71015
|
+
`[SecureChannel] ${label}: commit ${sentCommits + 1}/${commits.length} at epoch ${Number(c2.epoch)} could not be verified and was not refused \u2014 proceeding UNCHECKED (#1184)`
|
|
71016
|
+
);
|
|
71017
|
+
} else {
|
|
71018
|
+
refusedAt = sentCommits;
|
|
71019
|
+
break;
|
|
71020
|
+
}
|
|
71021
|
+
parent2 = ours;
|
|
71022
|
+
sentCommits++;
|
|
71023
|
+
}
|
|
71024
|
+
const cutFromRefusal = () => {
|
|
71025
|
+
const r2 = inFlight.refusal;
|
|
71026
|
+
if (r2 === void 0) return refusedAt;
|
|
71027
|
+
return refusedAt === -1 ? r2.from : Math.min(refusedAt, r2.from);
|
|
71028
|
+
};
|
|
71029
|
+
const rollBackTo = async (cut2, stage, persisted) => {
|
|
71030
|
+
const why = inFlight.refusal?.detail ?? verdict.toUpperCase();
|
|
71031
|
+
mlsGroup.importState(inFlight.sent[cut2].stateBefore);
|
|
71032
|
+
sentCommits = cut2;
|
|
70927
71033
|
console.warn(
|
|
70928
|
-
`[SecureChannel] ${label}:
|
|
71034
|
+
`[SecureChannel] ${label}: commit ${cut2 + 1}/${commits.length} ${stage} \u2014 "${why}" \u2014 rolled back to epoch ${Number(mlsGroup.epoch)}` + (cut2 === 0 ? ", nothing" : ` (after commit ${cut2}), only that`) + ` persisted, ${targets.length} request(s) stay pending`
|
|
70929
71035
|
);
|
|
70930
|
-
|
|
71036
|
+
if (persisted) await saveMlsState(this.config.dataDir, mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
71037
|
+
};
|
|
71038
|
+
let cut = cutFromRefusal();
|
|
71039
|
+
if (cut !== -1) {
|
|
71040
|
+
await rollBackTo(cut, "was refused by the server", false);
|
|
71041
|
+
if (sentCommits === 0) return 0;
|
|
70931
71042
|
}
|
|
70932
|
-
if (
|
|
70933
|
-
|
|
71043
|
+
if (sentCommits === 0) {
|
|
71044
|
+
mlsGroup.importState(snapshot);
|
|
71045
|
+
console.warn(
|
|
71046
|
+
`[SecureChannel] ${label}: commit was NOT written \u2014 rolled back to epoch ${Number(mlsGroup.epoch)}, nothing fulfilled, requests stay pending`
|
|
71047
|
+
);
|
|
71048
|
+
return 0;
|
|
71049
|
+
}
|
|
71050
|
+
await saveMlsState(this.config.dataDir, mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
71051
|
+
cut = cutFromRefusal();
|
|
71052
|
+
if (cut !== -1 && cut < sentCommits) {
|
|
71053
|
+
await rollBackTo(cut, "was refused by the server after the window (during persist)", true);
|
|
71054
|
+
return 0;
|
|
71055
|
+
}
|
|
71056
|
+
if (sentCommits < commits.length) {
|
|
71057
|
+
console.warn(
|
|
71058
|
+
`[SecureChannel] ${label}: only ${sentCommits}/${commits.length} commits were confirmed \u2014 requests stay PENDING for the next pull`
|
|
71059
|
+
);
|
|
71060
|
+
return 0;
|
|
71061
|
+
}
|
|
71062
|
+
if (!welcome) {
|
|
71063
|
+
console.warn(
|
|
71064
|
+
`[SecureChannel] ${label}: addMembers produced NO Welcome \u2014 ${targets.length} request(s) left pending rather than marked fulfilled`
|
|
71065
|
+
);
|
|
71066
|
+
return 0;
|
|
71067
|
+
}
|
|
71068
|
+
let fulfilled = 0;
|
|
71069
|
+
for (const t2 of targets) {
|
|
71070
|
+
const sent = await this._sendFrameConfirmed(
|
|
71071
|
+
{
|
|
71072
|
+
event: "mls_welcome",
|
|
71073
|
+
data: {
|
|
71074
|
+
target_device_id: t2.deviceId,
|
|
71075
|
+
group_id: mlsGroupId,
|
|
71076
|
+
...welcomeFields,
|
|
71077
|
+
payload: Buffer.from(welcome).toString("hex")
|
|
71078
|
+
}
|
|
71079
|
+
},
|
|
71080
|
+
`${label} welcome\u2192${t2.deviceId.slice(0, 8)}`
|
|
71081
|
+
);
|
|
71082
|
+
if (!sent) {
|
|
71083
|
+
console.warn(
|
|
71084
|
+
`[SecureChannel] ${label}: Welcome for ${t2.deviceId.slice(0, 8)} was NOT written \u2014 leaving the request PENDING so the next pull retries it`
|
|
71085
|
+
);
|
|
71086
|
+
continue;
|
|
71087
|
+
}
|
|
71088
|
+
cut = cutFromRefusal();
|
|
71089
|
+
if (cut !== -1 && cut < sentCommits) {
|
|
71090
|
+
await rollBackTo(cut, "was refused by the server after the window (during the Welcome)", true);
|
|
71091
|
+
return fulfilled;
|
|
71092
|
+
}
|
|
71093
|
+
if (await this._postFulfilWelcome(mlsGroupId, t2.requestId, `${label} ${t2.deviceId.slice(0, 8)}`)) {
|
|
71094
|
+
fulfilled++;
|
|
71095
|
+
}
|
|
71096
|
+
}
|
|
71097
|
+
return fulfilled;
|
|
71098
|
+
} finally {
|
|
71099
|
+
this._commitInFlight.delete(mlsGroupId);
|
|
71100
|
+
for (const frame of inFlight.frames) {
|
|
71101
|
+
try {
|
|
71102
|
+
await this._handleMlsCommit(frame);
|
|
71103
|
+
} catch (err) {
|
|
71104
|
+
console.error(`[SecureChannel] ${label}: replaying a held MLS commit failed:`, err);
|
|
71105
|
+
}
|
|
70934
71106
|
}
|
|
70935
71107
|
}
|
|
70936
|
-
return fulfilled;
|
|
70937
71108
|
}
|
|
70938
71109
|
/**
|
|
70939
71110
|
* Resolve an MLS group id to the family that owns it.
|
|
@@ -70980,24 +71151,237 @@ ${baseText}`;
|
|
|
70980
71151
|
/**
|
|
70981
71152
|
* #1177: the digest of the commit at the server's head, or undefined.
|
|
70982
71153
|
*
|
|
70983
|
-
* Undefined covers "no head yet"
|
|
70984
|
-
*
|
|
70985
|
-
*
|
|
70986
|
-
* so a read failure must degrade
|
|
70987
|
-
* rescue that cannot run is the
|
|
71154
|
+
* Undefined covers "no head yet" (a pruned group: epoch known, digest NULL —
|
|
71155
|
+
* 29 in production) AND "could not read" — deliberately the same value. The
|
|
71156
|
+
* commit is sent either way: the server skips the parent check for an
|
|
71157
|
+
* ABSENT field and refuses a MALFORMED one, so a read failure must degrade
|
|
71158
|
+
* to "unchecked", never to "blocked" (a rescue that cannot run is the
|
|
71159
|
+
* 2026-09-04 lockout) and never to `null`.
|
|
70988
71160
|
*/
|
|
70989
71161
|
async _readGroupHeadDigest(groupId) {
|
|
71162
|
+
const head = await this._readGroupHead(groupId);
|
|
71163
|
+
return head.ok ? head.digest : void 0;
|
|
71164
|
+
}
|
|
71165
|
+
/**
|
|
71166
|
+
* #1184: the server's head — its `current_epoch` and the digest of the commit
|
|
71167
|
+
* that produced it.
|
|
71168
|
+
*
|
|
71169
|
+
* - `{ ok: true, epoch, digest? }` the server described its head. `digest`
|
|
71170
|
+
* is absent on a pruned group (NULL
|
|
71171
|
+
* `last_commit_digest`).
|
|
71172
|
+
* - `{ ok: false, why }` the read failed: a non-2xx (the 403
|
|
71173
|
+
* `assert_group_member` returns on an
|
|
71174
|
+
* EMPTY roster — 79 of 80 production
|
|
71175
|
+
* rooms — or a 5xx), a timeout, or a body
|
|
71176
|
+
* with no `current_epoch`. Callers may
|
|
71177
|
+
* retry; a failure is never a verdict.
|
|
71178
|
+
*
|
|
71179
|
+
* `current_epoch` is a required field of `MLSGroupResponse`, so "200 with
|
|
71180
|
+
* no head" is not a shape any deployed backend produces; it is folded into
|
|
71181
|
+
* `ok: false` rather than given a branch of its own (the first cut of #1184
|
|
71182
|
+
* had one, and it was the only branch the suite exercised).
|
|
71183
|
+
*/
|
|
71184
|
+
async _readGroupHead(groupId) {
|
|
70990
71185
|
try {
|
|
70991
71186
|
const res = await fetch(`${this.config.apiUrl}/api/v1/mls/groups/${groupId}`, {
|
|
70992
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}` }
|
|
71187
|
+
headers: { Authorization: `Bearer ${this._deviceJwt}` },
|
|
71188
|
+
// The confirm loop holds `_deliveryPulling`; a black-holed read must
|
|
71189
|
+
// not hold it for undici's 300 s default. A timeout is a failed read.
|
|
71190
|
+
signal: AbortSignal.timeout(_SecureChannel.HEAD_READ_TIMEOUT_MS)
|
|
70993
71191
|
});
|
|
70994
|
-
if (!res.ok) return
|
|
71192
|
+
if (!res.ok) return { ok: false, why: `HTTP ${res.status}` };
|
|
70995
71193
|
const head = await res.json();
|
|
70996
|
-
|
|
70997
|
-
|
|
70998
|
-
|
|
70999
|
-
|
|
71194
|
+
if (typeof head?.current_epoch !== "number") return { ok: false, why: "no current_epoch in body" };
|
|
71195
|
+
const d2 = head.last_commit_digest;
|
|
71196
|
+
return { ok: true, epoch: head.current_epoch, digest: typeof d2 === "string" && /^[0-9a-f]{64}$/.test(d2) ? d2 : void 0 };
|
|
71197
|
+
} catch (err) {
|
|
71198
|
+
return { ok: false, why: err instanceof Error ? err.name : String(err) };
|
|
71199
|
+
}
|
|
71200
|
+
}
|
|
71201
|
+
/**
|
|
71202
|
+
* #1184: after each read of the head, how long to wait before the next.
|
|
71203
|
+
* The web's schedule (`lib/mls-commit-confirm.ts`) plus one immediate read:
|
|
71204
|
+
* five reads, ~3.75 s, no resend — an MLS commit cannot be regenerated. The
|
|
71205
|
+
* immediate read catches the common case where the server stored the frame
|
|
71206
|
+
* before the GET lands. Instance-level so tests can run the decision logic
|
|
71207
|
+
* without the clock; an empty schedule still reads once (`_confirmCommitAtHead`).
|
|
71208
|
+
*/
|
|
71209
|
+
_commitConfirmDelaysMs = [0, 250, 500, 1e3, 2e3];
|
|
71210
|
+
static HEAD_READ_TIMEOUT_MS = 5e3;
|
|
71211
|
+
/**
|
|
71212
|
+
* #1184: groups with a commit of OURS between "built" and "confirmed or rolled
|
|
71213
|
+
* back". Each record holds the `mls_commit` frames the socket delivered for
|
|
71214
|
+
* that group meanwhile — before the confirm loop that window was
|
|
71215
|
+
* microseconds; now it is up to ~3.75 s, and a commit applied to our
|
|
71216
|
+
* unconfirmed local state would be lost by the rollback (in memory) after
|
|
71217
|
+
* being persisted (on disk). Held frames are replayed, in order, once the
|
|
71218
|
+
* group is back on a real state. The record also carries the server's
|
|
71219
|
+
* REFUSAL, when one arrives on the socket (`_noteCommitRefusal`), and a
|
|
71220
|
+
* `wake` the confirm loop installs so a refusal ends its wait at once.
|
|
71221
|
+
*/
|
|
71222
|
+
_commitInFlight = /* @__PURE__ */ new Map();
|
|
71223
|
+
/** #1184: a pull was requested while one was running — run once more after. */
|
|
71224
|
+
_deliveryPullRequested = false;
|
|
71225
|
+
/**
|
|
71226
|
+
* #1184: an `error` frame naming a group we have a commit in flight for, with
|
|
71227
|
+
* a `… commit rejected` detail, is the server's refusal of one of those
|
|
71228
|
+
* commits — every rejection the `mls_commit` WS handler sends (malformed
|
|
71229
|
+
* parent, PARENT_MISMATCH, EPOCH_CONFLICT, not-a-member, no-participants)
|
|
71230
|
+
* ends with those words and carries `group_id` (pinned by
|
|
71231
|
+
* `tests/test_1184_commit_reject_frame_contract.py`). It is sent on OUR
|
|
71232
|
+
* socket after the server processed OUR frame, so it is deterministic in a
|
|
71233
|
+
* way the head poll is not, and it is the only refusal signal on a group
|
|
71234
|
+
* whose head we cannot read (403).
|
|
71235
|
+
*
|
|
71236
|
+
* WHICH commit, and what else the frame proves (re-review of #1227):
|
|
71237
|
+
* - EPOCH_CONFLICT carries `expected_epoch` (the epoch we claimed to build
|
|
71238
|
+
* on) and `current_epoch` (the server's); PARENT_MISMATCH carries
|
|
71239
|
+
* `current_epoch` (the head, which equalled our claim or the epoch check
|
|
71240
|
+
* would have fired first) and `head_digest`. The refused commit is the
|
|
71241
|
+
* one built on that epoch. A frame naming an epoch none of our in-flight
|
|
71242
|
+
* commits claimed is a refusal of some EARLIER attempt — ignored.
|
|
71243
|
+
* - the server's `current_epoch` bounds our earlier commits too: any of
|
|
71244
|
+
* ours ABOVE it did not land.
|
|
71245
|
+
* - `head_digest` at `current_epoch` is our commit at that epoch → that one
|
|
71246
|
+
* positively LANDED; someone else's → ours at that epoch did not.
|
|
71247
|
+
* - the un-numbered shapes refuse all of ours alike: the cut is the first
|
|
71248
|
+
* commit the head poll has not positively confirmed.
|
|
71249
|
+
* Recorded as the index of the first commit shown not to hold; the fulfil
|
|
71250
|
+
* path honours it whenever it arrives. A frame for a group with nothing in
|
|
71251
|
+
* flight is somebody else's business.
|
|
71252
|
+
*/
|
|
71253
|
+
_noteCommitRefusal(payload) {
|
|
71254
|
+
const groupId = typeof payload?.group_id === "string" ? payload.group_id : void 0;
|
|
71255
|
+
const detail = typeof payload?.detail === "string" ? payload.detail : "";
|
|
71256
|
+
if (!groupId || !detail.endsWith("commit rejected")) return;
|
|
71257
|
+
const inFlight = this._commitInFlight.get(groupId);
|
|
71258
|
+
if (!inFlight) return;
|
|
71259
|
+
const num = (v2) => typeof v2 === "number" && Number.isFinite(v2) ? v2 : void 0;
|
|
71260
|
+
const current = num(payload.current_epoch);
|
|
71261
|
+
const claimed = num(payload.expected_epoch) ?? current;
|
|
71262
|
+
const headDigest = typeof payload.head_digest === "string" ? payload.head_digest : void 0;
|
|
71263
|
+
const sent = inFlight.sent;
|
|
71264
|
+
const tag = `[SecureChannel] refusal for ${groupId.slice(0, 8)} ("${detail}")`;
|
|
71265
|
+
let from = -1;
|
|
71266
|
+
if (claimed !== void 0) {
|
|
71267
|
+
from = sent.findIndex((c2) => c2.epoch === claimed + 1);
|
|
71268
|
+
if (from === -1) {
|
|
71269
|
+
console.warn(`${tag} names epoch ${claimed + 1}, which no commit in flight claimed \u2014 an earlier attempt's; ignored`);
|
|
71270
|
+
return;
|
|
71271
|
+
}
|
|
71272
|
+
}
|
|
71273
|
+
if (current !== void 0) {
|
|
71274
|
+
const above = sent.findIndex((c2) => c2.epoch > current && !c2.verified);
|
|
71275
|
+
if (above !== -1 && (from === -1 || above < from)) from = above;
|
|
71276
|
+
}
|
|
71277
|
+
if (headDigest !== void 0 && current !== void 0) {
|
|
71278
|
+
const at = sent.findIndex((c2) => c2.epoch === current);
|
|
71279
|
+
if (at !== -1) {
|
|
71280
|
+
if (sent[at].digest === headDigest) {
|
|
71281
|
+
sent[at].verified = true;
|
|
71282
|
+
} else if (from === -1 || at < from) {
|
|
71283
|
+
from = at;
|
|
71284
|
+
}
|
|
71285
|
+
}
|
|
71286
|
+
}
|
|
71287
|
+
if (from === -1) from = sent.findIndex((c2) => !c2.verified);
|
|
71288
|
+
if (from === -1 || sent[from].verified) {
|
|
71289
|
+
console.warn(`${tag} contradicts a head read that showed our commit \u2014 stale; ignored`);
|
|
71290
|
+
return;
|
|
71291
|
+
}
|
|
71292
|
+
if (inFlight.refusal !== void 0 && inFlight.refusal.from <= from) return;
|
|
71293
|
+
inFlight.refusal = { detail, from };
|
|
71294
|
+
inFlight.wake?.();
|
|
71295
|
+
}
|
|
71296
|
+
/**
|
|
71297
|
+
* #1184: a commit is CONFIRMED when the server's head shows OUR digest at OUR
|
|
71298
|
+
* epoch — never when the socket took the bytes (#1015 covers only that). The
|
|
71299
|
+
* server sends no ack for a stored commit, so, like the web, we read the
|
|
71300
|
+
* head back; but it DOES send a refusal on the socket (`_noteCommitRefusal`),
|
|
71301
|
+
* and that is checked first, before every read and during every wait.
|
|
71302
|
+
*
|
|
71303
|
+
* confirmed head.epoch === ours && head.digest === ours
|
|
71304
|
+
* conflict the server refused ours on the socket, OR the head is at
|
|
71305
|
+
* our epoch with ANOTHER digest, OR already past us
|
|
71306
|
+
* unconfirmed the head was READ (at least once) but never reached our
|
|
71307
|
+
* epoch within the window — the server described its state
|
|
71308
|
+
* and ours is not in it
|
|
71309
|
+
* unverifiable the head could not be read on ANY try (403 on an empty
|
|
71310
|
+
* roster, an outage) and no refusal arrived — or it is at
|
|
71311
|
+
* our epoch but keeps no digest (a pruned group). Nothing
|
|
71312
|
+
* can be decided on; the caller proceeds UNCHECKED (#1177's
|
|
71313
|
+
* rule: a rescue that cannot run is the 2026-09-04 lockout)
|
|
71314
|
+
*
|
|
71315
|
+
* The first cut returned `unverifiable` only for a body with no
|
|
71316
|
+
* `current_epoch`, which no deployed backend produces, and rolled back on
|
|
71317
|
+
* every real read failure — the opposite of the rule it stated.
|
|
71318
|
+
*/
|
|
71319
|
+
async _confirmCommitAtHead(groupId, epoch, ours, inFlight, label) {
|
|
71320
|
+
const refused = () => {
|
|
71321
|
+
if (inFlight.refusal === void 0) return void 0;
|
|
71322
|
+
console.warn(`[SecureChannel] ${label}: server refused our commit at epoch ${epoch} \u2014 "${inFlight.refusal.detail}"`);
|
|
71323
|
+
return "conflict";
|
|
71324
|
+
};
|
|
71325
|
+
let lastSeen;
|
|
71326
|
+
let described = false;
|
|
71327
|
+
let lastFailure = "";
|
|
71328
|
+
const delays = this._commitConfirmDelaysMs.length > 0 ? this._commitConfirmDelaysMs : [0];
|
|
71329
|
+
for (const delay of delays) {
|
|
71330
|
+
if (delay > 0) {
|
|
71331
|
+
await new Promise((resolve3) => {
|
|
71332
|
+
const timer = setTimeout(() => {
|
|
71333
|
+
inFlight.wake = void 0;
|
|
71334
|
+
resolve3();
|
|
71335
|
+
}, delay);
|
|
71336
|
+
inFlight.wake = () => {
|
|
71337
|
+
clearTimeout(timer);
|
|
71338
|
+
inFlight.wake = void 0;
|
|
71339
|
+
resolve3();
|
|
71340
|
+
};
|
|
71341
|
+
});
|
|
71342
|
+
}
|
|
71343
|
+
const early = refused();
|
|
71344
|
+
if (early) return early;
|
|
71345
|
+
const head = await this._readGroupHead(groupId);
|
|
71346
|
+
const duringRead = refused();
|
|
71347
|
+
if (duringRead) return duringRead;
|
|
71348
|
+
if (!head.ok) {
|
|
71349
|
+
lastFailure = head.why;
|
|
71350
|
+
continue;
|
|
71351
|
+
}
|
|
71352
|
+
described = true;
|
|
71353
|
+
lastSeen = head.epoch;
|
|
71354
|
+
if (head.epoch === epoch) {
|
|
71355
|
+
if (head.digest === ours) return "confirmed";
|
|
71356
|
+
if (head.digest === void 0) return "unverifiable";
|
|
71357
|
+
console.warn(
|
|
71358
|
+
`[SecureChannel] ${label}: head at epoch ${epoch} is NOT our commit (${head.digest.slice(0, 12)}\u2026 vs ours ${ours.slice(0, 12)}\u2026) \u2014 ours was refused`
|
|
71359
|
+
);
|
|
71360
|
+
return "conflict";
|
|
71361
|
+
}
|
|
71362
|
+
if (head.epoch > epoch) {
|
|
71363
|
+
console.warn(`[SecureChannel] ${label}: head already at epoch ${head.epoch}, past our ${epoch} \u2014 ours was refused`);
|
|
71364
|
+
return "conflict";
|
|
71365
|
+
}
|
|
71366
|
+
}
|
|
71367
|
+
const late2 = refused();
|
|
71368
|
+
if (late2) return late2;
|
|
71369
|
+
if (!described && !this._wsIsOpen()) {
|
|
71370
|
+
console.warn(
|
|
71371
|
+
`[SecureChannel] ${label}: the socket closed during the confirm window and the head could not be read \u2014 a refusal of commit at epoch ${epoch} may have been lost; treating as refused`
|
|
71372
|
+
);
|
|
71373
|
+
return "unconfirmed";
|
|
71374
|
+
}
|
|
71375
|
+
if (!described) {
|
|
71376
|
+
console.warn(
|
|
71377
|
+
`[SecureChannel] ${label}: could not read the head on any of ${delays.length} tries (last: ${lastFailure}) and the server sent no refusal \u2014 cannot verify commit at epoch ${epoch}`
|
|
71378
|
+
);
|
|
71379
|
+
return "unverifiable";
|
|
71000
71380
|
}
|
|
71381
|
+
console.warn(
|
|
71382
|
+
`[SecureChannel] ${label}: commit at epoch ${epoch} never appeared at the head (last seen ${lastSeen}) \u2014 treating as refused`
|
|
71383
|
+
);
|
|
71384
|
+
return "unconfirmed";
|
|
71001
71385
|
}
|
|
71002
71386
|
/**
|
|
71003
71387
|
* #1015: write one frame and report whether the socket took it.
|
|
@@ -71519,7 +71903,11 @@ ${baseText}`;
|
|
|
71519
71903
|
* Called on WS connect, on mls_delivery ping, and every 30s heartbeat.
|
|
71520
71904
|
*/
|
|
71521
71905
|
async _pullDeliveryQueue() {
|
|
71522
|
-
if (!this._deviceId || !this._deviceJwt
|
|
71906
|
+
if (!this._deviceId || !this._deviceJwt) return;
|
|
71907
|
+
if (this._deliveryPulling) {
|
|
71908
|
+
this._deliveryPullRequested = true;
|
|
71909
|
+
return;
|
|
71910
|
+
}
|
|
71523
71911
|
this._deliveryPulling = true;
|
|
71524
71912
|
try {
|
|
71525
71913
|
const res = await fetch(
|
|
@@ -72081,6 +72469,10 @@ ${baseText}`;
|
|
|
72081
72469
|
console.warn("[SecureChannel] Delivery pull error:", err);
|
|
72082
72470
|
} finally {
|
|
72083
72471
|
this._deliveryPulling = false;
|
|
72472
|
+
if (this._deliveryPullRequested) {
|
|
72473
|
+
this._deliveryPullRequested = false;
|
|
72474
|
+
void this._pullDeliveryQueue().catch((err) => console.warn("[SecureChannel] Coalesced delivery pull error:", err));
|
|
72475
|
+
}
|
|
72084
72476
|
}
|
|
72085
72477
|
}
|
|
72086
72478
|
/**
|
|
@@ -72164,7 +72556,7 @@ ${baseText}`;
|
|
|
72164
72556
|
try {
|
|
72165
72557
|
const parsed = JSON.parse(plaintext);
|
|
72166
72558
|
messageType = parsed.type || "message";
|
|
72167
|
-
messageText = parsed.text
|
|
72559
|
+
messageText = parsed.text ?? plaintext;
|
|
72168
72560
|
} catch {
|
|
72169
72561
|
messageType = "message";
|
|
72170
72562
|
messageText = plaintext;
|
|
@@ -72513,7 +72905,7 @@ ${baseText}`;
|
|
|
72513
72905
|
try {
|
|
72514
72906
|
const parsed = JSON.parse(plaintext);
|
|
72515
72907
|
messageType = parsed.type || "message";
|
|
72516
|
-
messageText = parsed.text
|
|
72908
|
+
messageText = parsed.text ?? plaintext;
|
|
72517
72909
|
} catch {
|
|
72518
72910
|
messageType = "message";
|
|
72519
72911
|
messageText = plaintext;
|
|
@@ -72798,7 +73190,7 @@ ${baseText}`;
|
|
|
72798
73190
|
try {
|
|
72799
73191
|
const parsed = JSON.parse(plaintext);
|
|
72800
73192
|
messageType = parsed.type || "message";
|
|
72801
|
-
messageText = parsed.text
|
|
73193
|
+
messageText = parsed.text ?? plaintext;
|
|
72802
73194
|
} catch {
|
|
72803
73195
|
messageType = "message";
|
|
72804
73196
|
messageText = plaintext;
|
|
@@ -73398,58 +73790,6 @@ var init_gateway_send = __esm2({
|
|
|
73398
73790
|
init_openclaw_compat();
|
|
73399
73791
|
}
|
|
73400
73792
|
});
|
|
73401
|
-
function parseMentions(text) {
|
|
73402
|
-
const mentions = [];
|
|
73403
|
-
const re2 = new RegExp(MENTION_RE.source, MENTION_RE.flags);
|
|
73404
|
-
let match;
|
|
73405
|
-
while ((match = re2.exec(text)) !== null) {
|
|
73406
|
-
mentions.push(match[1].toLowerCase());
|
|
73407
|
-
}
|
|
73408
|
-
return mentions;
|
|
73409
|
-
}
|
|
73410
|
-
function resolveOwn(mention, names) {
|
|
73411
|
-
if (names.has(mention)) return mention;
|
|
73412
|
-
const parts = mention.split("-");
|
|
73413
|
-
for (let i2 = parts.length - 1; i2 > 0; i2--) {
|
|
73414
|
-
const candidate = parts.slice(0, i2).join("-");
|
|
73415
|
-
if (names.has(candidate)) return candidate;
|
|
73416
|
-
}
|
|
73417
|
-
return null;
|
|
73418
|
-
}
|
|
73419
|
-
function owesRoomReply(plaintext, agentName, accountId, senderIsAgent, roomDisplayName, ownDisplayName, selfResolved = true) {
|
|
73420
|
-
const mentions = parseMentions(plaintext);
|
|
73421
|
-
if (mentions.length === 0) return false;
|
|
73422
|
-
const ownNames = ownRoomNames(agentName, accountId, roomDisplayName, ownDisplayName);
|
|
73423
|
-
if (mentions.some((m2) => resolveOwn(m2, ownNames) !== null)) return true;
|
|
73424
|
-
if (mentions.some((m2) => resolveOwn(m2, BROADCAST_NAMES) !== null)) {
|
|
73425
|
-
return senderIsAgent !== true;
|
|
73426
|
-
}
|
|
73427
|
-
if (!selfResolved) return true;
|
|
73428
|
-
return false;
|
|
73429
|
-
}
|
|
73430
|
-
function ownRoomNames(agentName, accountId, roomDisplayName, ownDisplayName) {
|
|
73431
|
-
const names = /* @__PURE__ */ new Set();
|
|
73432
|
-
const add4 = (n2) => {
|
|
73433
|
-
if (!n2) return;
|
|
73434
|
-
const lower = n2.toLowerCase();
|
|
73435
|
-
names.add(lower);
|
|
73436
|
-
names.add(lower.split(/[\s(]/)[0]);
|
|
73437
|
-
};
|
|
73438
|
-
add4(agentName);
|
|
73439
|
-
add4(accountId);
|
|
73440
|
-
add4(roomDisplayName);
|
|
73441
|
-
add4(ownDisplayName);
|
|
73442
|
-
return names;
|
|
73443
|
-
}
|
|
73444
|
-
var MENTION_RE;
|
|
73445
|
-
var BROADCAST_NAMES;
|
|
73446
|
-
var init_room_protocol = __esm2({
|
|
73447
|
-
"src/room-protocol.ts"() {
|
|
73448
|
-
"use strict";
|
|
73449
|
-
MENTION_RE = /@([\w-]+)/gi;
|
|
73450
|
-
BROADCAST_NAMES = /* @__PURE__ */ new Set(["all", "everyone"]);
|
|
73451
|
-
}
|
|
73452
|
-
});
|
|
73453
73793
|
var init_llm_response_parser = __esm2({
|
|
73454
73794
|
"src/llm-response-parser.ts"() {
|
|
73455
73795
|
"use strict";
|
|
@@ -99543,7 +99883,7 @@ var init_index = __esm2({
|
|
|
99543
99883
|
await init_skill_telemetry();
|
|
99544
99884
|
await init_policy_enforcer();
|
|
99545
99885
|
init_room_protocol();
|
|
99546
|
-
VERSION = true ? "0.23.
|
|
99886
|
+
VERSION = true ? "0.23.40" : "0.0.0-dev";
|
|
99547
99887
|
}
|
|
99548
99888
|
});
|
|
99549
99889
|
await init_index();
|
|
@@ -100480,7 +100820,7 @@ async function main() {
|
|
|
100480
100820
|
const info = parseInfoFlags(process.argv.slice(2));
|
|
100481
100821
|
if (info.showVersion || info.showHelp) {
|
|
100482
100822
|
const version2 = buildClientVersion2(
|
|
100483
|
-
true ? "0.4.
|
|
100823
|
+
true ? "0.4.13" : void 0
|
|
100484
100824
|
);
|
|
100485
100825
|
if (info.showVersion) printLine(version2);
|
|
100486
100826
|
if (info.showHelp) {
|
|
@@ -100533,7 +100873,7 @@ async function main() {
|
|
|
100533
100873
|
agentName: cfg.agentName,
|
|
100534
100874
|
platform: "node",
|
|
100535
100875
|
clientVersion: buildClientVersion2(
|
|
100536
|
-
true ? "0.4.
|
|
100876
|
+
true ? "0.4.13" : void 0
|
|
100537
100877
|
)
|
|
100538
100878
|
});
|
|
100539
100879
|
channelRef = channel;
|
package/package.json
CHANGED