@agentvault/grok-bridge 0.4.0 → 0.4.2
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 +6 -3
- package/dist/index.js +794 -234
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1074,6 +1074,28 @@ var __toESM = (mod4, isNodeMode, target) => (target = mod4 != null ? __create(__
|
|
|
1074
1074
|
isNodeMode || !mod4 || !mod4.__esModule ? __defProp2(target, "default", { value: mod4, enumerable: true }) : target,
|
|
1075
1075
|
mod4
|
|
1076
1076
|
));
|
|
1077
|
+
function stripWrappers(s2) {
|
|
1078
|
+
let start = 0;
|
|
1079
|
+
let end = s2.length;
|
|
1080
|
+
while (start < end && WRAPPERS.includes(s2[start])) start++;
|
|
1081
|
+
while (end > start && WRAPPERS.includes(s2[end - 1])) end--;
|
|
1082
|
+
return s2.slice(start, end);
|
|
1083
|
+
}
|
|
1084
|
+
function isAbstention(text) {
|
|
1085
|
+
if (typeof text !== "string") return false;
|
|
1086
|
+
return stripWrappers(text).toLowerCase().startsWith(SENTINEL);
|
|
1087
|
+
}
|
|
1088
|
+
var SENTINEL;
|
|
1089
|
+
var WRAPPERS;
|
|
1090
|
+
var INSTRUCTION;
|
|
1091
|
+
var init_abstention = __esm2({
|
|
1092
|
+
"src/abstention.ts"() {
|
|
1093
|
+
"use strict";
|
|
1094
|
+
SENTINEL = "[[no-reply]]";
|
|
1095
|
+
WRAPPERS = " \r\n`\"'*_";
|
|
1096
|
+
INSTRUCTION = "not every message needs an answer from you: if this one is not addressed to you and asks nothing of you, reply with exactly " + SENTINEL + " and nothing else \u2014 that is discarded, not posted, and is how you stay quiet; never post a message SAYING that you are staying quiet";
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1077
1099
|
function toEpoch(v2) {
|
|
1078
1100
|
if (typeof v2 === "bigint") return v2;
|
|
1079
1101
|
if (typeof v2 === "number") {
|
|
@@ -45899,6 +45921,11 @@ var init_ratchet = __esm2({
|
|
|
45899
45921
|
return { header, headerSignature, ciphertext, nonce };
|
|
45900
45922
|
}
|
|
45901
45923
|
decrypt(message) {
|
|
45924
|
+
if (this.state.peerIdentityPublicKey) {
|
|
45925
|
+
if (!verifyHeaderSignature(message.header, message.headerSignature, this.state.peerIdentityPublicKey)) {
|
|
45926
|
+
throw new Error("Header signature verification failed");
|
|
45927
|
+
}
|
|
45928
|
+
}
|
|
45902
45929
|
const isV2 = message.envelopeVersion === "2.0.0" && message.encryptedHeader != null && message.headerNonce != null;
|
|
45903
45930
|
const skippedResult = this.trySkippedKeys(message, isV2);
|
|
45904
45931
|
if (skippedResult !== null) {
|
|
@@ -45912,14 +45939,14 @@ var init_ratchet = __esm2({
|
|
|
45912
45939
|
if (message.header.messageNumber === 0) {
|
|
45913
45940
|
try {
|
|
45914
45941
|
const { messageKey: testKey, headerKey: testHeaderKey, nextChainKey: nextChainKey2 } = kdfChainKey(this.state.rootKey);
|
|
45915
|
-
let
|
|
45942
|
+
let ad;
|
|
45916
45943
|
if (isV2) {
|
|
45917
45944
|
libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.encryptedHeader, null, message.headerNonce, testHeaderKey);
|
|
45918
|
-
|
|
45945
|
+
ad = message.encryptedHeader;
|
|
45919
45946
|
} else {
|
|
45920
|
-
|
|
45947
|
+
ad = serializeHeader(message.header);
|
|
45921
45948
|
}
|
|
45922
|
-
const ptBytes = libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.ciphertext,
|
|
45949
|
+
const ptBytes = libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.ciphertext, ad, message.nonce, testKey);
|
|
45923
45950
|
this.state.dhReceivingPublicKey = message.header.dhPublicKey;
|
|
45924
45951
|
this.state.receivingChain = {
|
|
45925
45952
|
chainKey: nextChainKey2,
|
|
@@ -45939,13 +45966,7 @@ var init_ratchet = __esm2({
|
|
|
45939
45966
|
this.skipMessages(this.state.receivingChain, message.header.messageNumber, message.header.dhPublicKey, isV2);
|
|
45940
45967
|
const chain = this.state.receivingChain;
|
|
45941
45968
|
const { nextChainKey, messageKey, headerKey } = kdfChainKey(chain.chainKey);
|
|
45942
|
-
|
|
45943
|
-
chain.messageNumber++;
|
|
45944
|
-
if (this.state.peerIdentityPublicKey) {
|
|
45945
|
-
if (!verifyHeaderSignature(message.header, message.headerSignature, this.state.peerIdentityPublicKey)) {
|
|
45946
|
-
throw new Error("Header signature verification failed");
|
|
45947
|
-
}
|
|
45948
|
-
}
|
|
45969
|
+
let plaintextBytes;
|
|
45949
45970
|
if (isV2) {
|
|
45950
45971
|
try {
|
|
45951
45972
|
libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.encryptedHeader, null, message.headerNonce, headerKey);
|
|
@@ -45953,19 +45974,21 @@ var init_ratchet = __esm2({
|
|
|
45953
45974
|
throw new Error("V2 header decryption failed");
|
|
45954
45975
|
}
|
|
45955
45976
|
try {
|
|
45956
|
-
|
|
45957
|
-
return libsodium_wrappers_default.to_string(plaintextBytes);
|
|
45977
|
+
plaintextBytes = libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.ciphertext, message.encryptedHeader, message.nonce, messageKey);
|
|
45958
45978
|
} catch {
|
|
45959
45979
|
throw new Error("V2 decryption failed: ciphertext tampered or wrong key");
|
|
45960
45980
|
}
|
|
45981
|
+
} else {
|
|
45982
|
+
const ad = serializeHeader(message.header);
|
|
45983
|
+
try {
|
|
45984
|
+
plaintextBytes = libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.ciphertext, ad, message.nonce, messageKey);
|
|
45985
|
+
} catch {
|
|
45986
|
+
throw new Error("Decryption failed: ciphertext tampered or wrong key");
|
|
45987
|
+
}
|
|
45961
45988
|
}
|
|
45962
|
-
|
|
45963
|
-
|
|
45964
|
-
|
|
45965
|
-
return libsodium_wrappers_default.to_string(plaintextBytes);
|
|
45966
|
-
} catch {
|
|
45967
|
-
throw new Error("Decryption failed: ciphertext tampered or wrong key");
|
|
45968
|
-
}
|
|
45989
|
+
chain.chainKey = nextChainKey;
|
|
45990
|
+
chain.messageNumber++;
|
|
45991
|
+
return libsodium_wrappers_default.to_string(plaintextBytes);
|
|
45969
45992
|
}
|
|
45970
45993
|
dhRatchetReceive(theirDhPublic) {
|
|
45971
45994
|
this.state.previousSendingChainLength = this.state.sendingChain?.messageNumber ?? 0;
|
|
@@ -63751,6 +63774,39 @@ var init_mls_group = __esm2({
|
|
|
63751
63774
|
const s2 = this._requireState();
|
|
63752
63775
|
return s2.groupContext.groupId;
|
|
63753
63776
|
}
|
|
63777
|
+
/**
|
|
63778
|
+
* Proof that this client actually holds the group's keys at its current
|
|
63779
|
+
* epoch. Throws if not initialized.
|
|
63780
|
+
*
|
|
63781
|
+
* RFC 9420 §8.2's `epoch_authenticator`, derived from the epoch secret. Two
|
|
63782
|
+
* properties make it the thing this codebase has been missing:
|
|
63783
|
+
*
|
|
63784
|
+
* * every member at the same epoch computes the SAME value, so it is
|
|
63785
|
+
* comparable across clients and can be checked by a server;
|
|
63786
|
+
* * it cannot be computed without the key schedule, so possessing it is
|
|
63787
|
+
* evidence of membership rather than a claim about it. RFC 9420 names
|
|
63788
|
+
* this as its purpose: "to authenticate the shared group state".
|
|
63789
|
+
*
|
|
63790
|
+
* It is safe to publish. It is a KDF output over the epoch secret, not the
|
|
63791
|
+
* secret, and the RFC intends it to be shown to out-of-band verifiers.
|
|
63792
|
+
*
|
|
63793
|
+
* WHY THIS EXISTS. `mls_groups.member_device_ids` records membership; the
|
|
63794
|
+
* ability to decrypt lives only in one browser's localStorage. Nothing
|
|
63795
|
+
* compared the two, and nothing could — so a device in the roster holding no
|
|
63796
|
+
* keys was indistinguishable from a healthy one, on every signal the server
|
|
63797
|
+
* has. Measured on production 2026-08-26: one owner device carried the keys
|
|
63798
|
+
* for 19 of the 21 owner leaves on the platform, and one 1:1 group had been
|
|
63799
|
+
* unreadable by its owner's browser for two months while every status column
|
|
63800
|
+
* read healthy.
|
|
63801
|
+
*
|
|
63802
|
+
* `epoch` alone cannot do this job: it is a number the client reports about
|
|
63803
|
+
* itself, it collides across groups, and a client that has lost its state
|
|
63804
|
+
* still remembers one.
|
|
63805
|
+
*/
|
|
63806
|
+
get epochAuthenticator() {
|
|
63807
|
+
const s2 = this._requireState();
|
|
63808
|
+
return s2.keySchedule.epochAuthenticator;
|
|
63809
|
+
}
|
|
63754
63810
|
/** The cipher suite name in use. */
|
|
63755
63811
|
get ciphersuiteName() {
|
|
63756
63812
|
return this._ciphersuiteName;
|
|
@@ -63901,11 +63957,84 @@ var init_mls_group = __esm2({
|
|
|
63901
63957
|
return { commit: commitBytes, welcome: welcomeBytes };
|
|
63902
63958
|
}
|
|
63903
63959
|
/**
|
|
63904
|
-
*
|
|
63960
|
+
* Admit N members with TWO commits total, replacing any stale leaf they hold.
|
|
63961
|
+
*
|
|
63962
|
+
* `addMembers` cannot do this alone: MLS rejects an Add for an identity
|
|
63963
|
+
* already in the tree, so a member that lost its local state must be removed
|
|
63964
|
+
* first. Doing that pair PER MEMBER is what the fulfiller did, and it is not
|
|
63965
|
+
* merely slow — it is self-defeating.
|
|
63966
|
+
*
|
|
63967
|
+
* Live 2026-08-28 14:02:21-28Z: the owner sweep re-admitted 7 room members
|
|
63968
|
+
* sequentially, each as its own removeMember + addMembers. Room 007 ran
|
|
63969
|
+
* **epoch 71 -> 85 in seven seconds**, and the Welcome minted for member n
|
|
63970
|
+
* named an epoch that member n+1's commit had already replaced — so six of
|
|
63971
|
+
* seven were stale before they finished joining, and one (`smartcode`) could
|
|
63972
|
+
* not replay forward afterwards at all.
|
|
63973
|
+
*
|
|
63974
|
+
* 🎯 A REPAIR THAT MOVES THE EPOCH ONCE PER MEMBER IS RACING ITSELF.
|
|
63905
63975
|
*
|
|
63906
|
-
*
|
|
63907
|
-
*
|
|
63976
|
+
* ⚠️ **Why two commits and not one.** RFC 9420 §12.3 allows Remove and Add in
|
|
63977
|
+
* a single commit, and ts-mls builds it happily — but with MORE THAN ONE of
|
|
63978
|
+
* each, a REMAINING member fails to process it:
|
|
63979
|
+
*
|
|
63980
|
+
* OpenError at applyUpdatePathSecret (ts-mls/src/createCommit.ts:471)
|
|
63981
|
+
* at updatePrivateKeyPath (processMessages.ts:397)
|
|
63982
|
+
*
|
|
63983
|
+
* One remove + one add in a single commit is fine; 2+2 is not. That is
|
|
63984
|
+
* pinned by a test in `mls-group.test.ts` so this cannot be "optimised" back
|
|
63985
|
+
* into one commit without the failure showing up first. Two commits still
|
|
63986
|
+
* turns 7 members from 14 epochs into 2, and every joiner lands current.
|
|
63987
|
+
*
|
|
63988
|
+
* @param keyPackages - Fresh KeyPackages, one per member to admit. Members
|
|
63989
|
+
* already holding a leaf are removed by the first commit; members with no
|
|
63990
|
+
* leaf are plain adds. Duplicate identities keep the first KeyPackage.
|
|
63991
|
+
* @returns Every commit to broadcast IN ORDER, and the Welcome to deliver to
|
|
63992
|
+
* EVERY member in the batch (each finds its own secrets inside it).
|
|
63908
63993
|
*/
|
|
63994
|
+
async reAdmitMembers(keyPackages) {
|
|
63995
|
+
this._requireState();
|
|
63996
|
+
await this._ensureCiphersuite();
|
|
63997
|
+
const seenIdentities = [];
|
|
63998
|
+
const toAdd = [];
|
|
63999
|
+
const leavesToRemove = [];
|
|
64000
|
+
for (const kp of keyPackages) {
|
|
64001
|
+
const identity = keyPackageIdentity(kp);
|
|
64002
|
+
if (identity !== null) {
|
|
64003
|
+
if (seenIdentities.some((id) => bytesEqual(id, identity)))
|
|
64004
|
+
continue;
|
|
64005
|
+
seenIdentities.push(identity);
|
|
64006
|
+
const leaf = this.memberLeafIndex(identity);
|
|
64007
|
+
if (leaf !== null)
|
|
64008
|
+
leavesToRemove.push(leaf);
|
|
64009
|
+
}
|
|
64010
|
+
toAdd.push(kp);
|
|
64011
|
+
}
|
|
64012
|
+
if (toAdd.length === 0) {
|
|
64013
|
+
throw new Error("reAdmitMembers: no members to admit");
|
|
64014
|
+
}
|
|
64015
|
+
const commits = [];
|
|
64016
|
+
if (leavesToRemove.length > 0) {
|
|
64017
|
+
const { commit } = await this.removeMembers(leavesToRemove);
|
|
64018
|
+
commits.push({ commit, epoch: this.epoch });
|
|
64019
|
+
}
|
|
64020
|
+
const added = await this.addMembers(toAdd);
|
|
64021
|
+
commits.push({ commit: added.commit, epoch: this.epoch });
|
|
64022
|
+
return { commits, welcome: added.welcome };
|
|
64023
|
+
}
|
|
64024
|
+
async removeMembers(memberIndices) {
|
|
64025
|
+
const s2 = this._requireState();
|
|
64026
|
+
const cs = await this._ensureCiphersuite();
|
|
64027
|
+
if (memberIndices.length === 0) {
|
|
64028
|
+
throw new Error("removeMembers: no members to remove");
|
|
64029
|
+
}
|
|
64030
|
+
const extraProposals = memberIndices.map((removed) => ({
|
|
64031
|
+
proposalType: "remove",
|
|
64032
|
+
remove: { removed }
|
|
64033
|
+
}));
|
|
64034
|
+
const result = await createCommit({ state: s2, cipherSuite: cs, pskIndex: this._pskIndex }, { extraProposals });
|
|
64035
|
+
this.state = result.newState;
|
|
64036
|
+
return { commit: encodeMlsMessage(result.commit) };
|
|
64037
|
+
}
|
|
63909
64038
|
async removeMember(memberIndex) {
|
|
63910
64039
|
const s2 = this._requireState();
|
|
63911
64040
|
const cs = await this._ensureCiphersuite();
|
|
@@ -64139,6 +64268,23 @@ var init_mls_delivery_order = __esm2({
|
|
|
64139
64268
|
"use strict";
|
|
64140
64269
|
}
|
|
64141
64270
|
});
|
|
64271
|
+
function noteObservedEpoch(current, seen) {
|
|
64272
|
+
const n2 = Number(seen);
|
|
64273
|
+
if (!Number.isInteger(n2) || n2 < 0)
|
|
64274
|
+
return current;
|
|
64275
|
+
return n2 > current ? n2 : current;
|
|
64276
|
+
}
|
|
64277
|
+
function needsPreSendResync(localEpoch, observedEpoch) {
|
|
64278
|
+
if (!Number.isInteger(localEpoch) || !Number.isInteger(observedEpoch)) {
|
|
64279
|
+
return false;
|
|
64280
|
+
}
|
|
64281
|
+
return observedEpoch > localEpoch;
|
|
64282
|
+
}
|
|
64283
|
+
var init_mls_presend_epoch = __esm2({
|
|
64284
|
+
"../crypto/dist/mls-presend-epoch.js"() {
|
|
64285
|
+
"use strict";
|
|
64286
|
+
}
|
|
64287
|
+
});
|
|
64142
64288
|
var dist_exports = {};
|
|
64143
64289
|
__export2(dist_exports, {
|
|
64144
64290
|
AV_CREDENTIAL_CONTEXT: () => AV_CREDENTIAL_CONTEXT,
|
|
@@ -64208,7 +64354,9 @@ __export2(dist_exports, {
|
|
|
64208
64354
|
hexTransportToEncryptedMessage: () => hexTransportToEncryptedMessage,
|
|
64209
64355
|
issueCredential: () => issueCredential,
|
|
64210
64356
|
multibaseToPublicKey: () => multibaseToPublicKey,
|
|
64357
|
+
needsPreSendResync: () => needsPreSendResync,
|
|
64211
64358
|
normalizeBackupCode: () => normalizeBackupCode,
|
|
64359
|
+
noteObservedEpoch: () => noteObservedEpoch,
|
|
64212
64360
|
orderDeliveryBatch: () => orderDeliveryBatch,
|
|
64213
64361
|
parseTraceparent: () => parseTraceparent,
|
|
64214
64362
|
performX3DH: () => performX3DH,
|
|
@@ -64248,6 +64396,7 @@ var init_dist = __esm2({
|
|
|
64248
64396
|
init_mls_group();
|
|
64249
64397
|
await init_owner_sync();
|
|
64250
64398
|
init_mls_delivery_order();
|
|
64399
|
+
init_mls_presend_epoch();
|
|
64251
64400
|
}
|
|
64252
64401
|
});
|
|
64253
64402
|
async function ensureSecureDir(dir) {
|
|
@@ -64440,7 +64589,7 @@ var init_mls_kp_pool = __esm2({
|
|
|
64440
64589
|
}
|
|
64441
64590
|
});
|
|
64442
64591
|
function ownIdentity() {
|
|
64443
|
-
const v2 = true ? "0.23.
|
|
64592
|
+
const v2 = true ? "0.23.29" : FALLBACK;
|
|
64444
64593
|
return `${PACKAGE}@${v2}`;
|
|
64445
64594
|
}
|
|
64446
64595
|
function buildClientVersion(override) {
|
|
@@ -65352,10 +65501,12 @@ var SecureChannel;
|
|
|
65352
65501
|
var init_channel = __esm2({
|
|
65353
65502
|
async "src/channel.ts"() {
|
|
65354
65503
|
"use strict";
|
|
65504
|
+
init_abstention();
|
|
65355
65505
|
init_mls_commit_classify();
|
|
65356
65506
|
await init_libsodium_wrappers();
|
|
65357
65507
|
await init_dist();
|
|
65358
65508
|
await init_dist();
|
|
65509
|
+
init_mls_presend_epoch();
|
|
65359
65510
|
init_mls_state();
|
|
65360
65511
|
await init_mls_kp_pool();
|
|
65361
65512
|
init_client_version();
|
|
@@ -65469,6 +65620,17 @@ var init_channel = __esm2({
|
|
|
65469
65620
|
* self-heal rather than be logged and dropped. */
|
|
65470
65621
|
_mlsCommitFailCounts = /* @__PURE__ */ new Map();
|
|
65471
65622
|
static MAX_MLS_DECRYPT_FAILS = 3;
|
|
65623
|
+
/**
|
|
65624
|
+
* #1084: consecutive resyncs that found NOTHING to apply, per group.
|
|
65625
|
+
*
|
|
65626
|
+
* "No new commits" means the server holds no commit past our epoch — we are
|
|
65627
|
+
* NOT BEHIND. Treating that as corruption and re-keying is what drove the
|
|
65628
|
+
* epoch churn (see `_resyncOnDivergence`). But a state that is genuinely
|
|
65629
|
+
* broken while its epoch counter happens to match must still recover, so the
|
|
65630
|
+
* suppression is bounded rather than absolute.
|
|
65631
|
+
*/
|
|
65632
|
+
_resyncNoOpStrikes = /* @__PURE__ */ new Map();
|
|
65633
|
+
static MAX_RESYNC_NOOPS = 3;
|
|
65472
65634
|
/** Cached MLS KeyPackage bundle for this device (regenerated on each connect). */
|
|
65473
65635
|
_mlsKeyPackage = null;
|
|
65474
65636
|
/** Pending KeyPackage bundle from request-Welcome flow (used by _handleMlsWelcome). */
|
|
@@ -65506,6 +65668,13 @@ var init_channel = __esm2({
|
|
|
65506
65668
|
_kpPoolFilling = false;
|
|
65507
65669
|
/** Buffer for MLS commits received before Welcome (keyed by groupId, sorted by epoch). */
|
|
65508
65670
|
_pendingMlsCommits = /* @__PURE__ */ new Map();
|
|
65671
|
+
/**
|
|
65672
|
+
* #1013: the highest group epoch any inbound frame has reported, per MLS
|
|
65673
|
+
* group id. Every inbound frame carries the epoch the group was at when it
|
|
65674
|
+
* was produced, so each is a LOWER BOUND on the group's current epoch — the
|
|
65675
|
+
* hint is monotonic and only ever rises.
|
|
65676
|
+
*/
|
|
65677
|
+
_observedGroupEpochs = /* @__PURE__ */ new Map();
|
|
65509
65678
|
/** In-memory credential store for renter-provided credentials (never persisted). */
|
|
65510
65679
|
_credentialStore = new CredentialStore();
|
|
65511
65680
|
/** Rooms whose roster has been refreshed from the backend at least once in
|
|
@@ -65762,6 +65931,18 @@ var init_channel = __esm2({
|
|
|
65762
65931
|
get ownHubAddress() {
|
|
65763
65932
|
return this._persisted?.hubAddress ?? "";
|
|
65764
65933
|
}
|
|
65934
|
+
/** This agent's AgentVault hub_id. "" until enrolment persists it.
|
|
65935
|
+
*
|
|
65936
|
+
* Lives in the credential store, NOT in the channel config — which is where
|
|
65937
|
+
* the memory-recall helper was reading it from. `channel.config.hubId` is
|
|
65938
|
+
* unset for every agent enrolled through the normal flow, so
|
|
65939
|
+
* `channel.config.hubId || ""` produced `/api/v1/agents//memory/search`, a
|
|
65940
|
+
* 404 on every inbound message, swallowed by `if (!res.ok) return ""`.
|
|
65941
|
+
* Memory recall was silently dead while the hub_id sat right here.
|
|
65942
|
+
*/
|
|
65943
|
+
get ownHubId() {
|
|
65944
|
+
return this._persisted?.hubId ?? "";
|
|
65945
|
+
}
|
|
65765
65946
|
/** Public best-effort trigger to (re)resolve this agent's display_name.
|
|
65766
65947
|
* Called lazily from the 1:1 DM path when the name is still unknown, so a
|
|
65767
65948
|
* resolve that failed at connect (or a fresh agent that hadn't registered
|
|
@@ -66150,6 +66331,7 @@ var init_channel = __esm2({
|
|
|
66150
66331
|
* Each session gets the same plaintext encrypted independently.
|
|
66151
66332
|
*/
|
|
66152
66333
|
async send(plaintext, options) {
|
|
66334
|
+
if (isAbstention(plaintext)) return;
|
|
66153
66335
|
if (this._state === "error" || this._state === "idle") {
|
|
66154
66336
|
throw new Error("Channel is not ready");
|
|
66155
66337
|
}
|
|
@@ -66190,16 +66372,44 @@ var init_channel = __esm2({
|
|
|
66190
66372
|
const pendingWsSends = [];
|
|
66191
66373
|
const sentSharedGroupIds = /* @__PURE__ */ new Set();
|
|
66192
66374
|
const addressedMlsGroupIds = [];
|
|
66375
|
+
if (!(this._persisted?.mlsGroups && this._state === "ready" && this._ws)) {
|
|
66376
|
+
console.warn(
|
|
66377
|
+
`[SecureChannel] send(): shared-MLS block SKIPPED \u2014 mlsGroups=${this._persisted?.mlsGroups ? Object.keys(this._persisted.mlsGroups).length : "none"} state=${this._state} ws=${this._ws ? "open" : "null"}`
|
|
66378
|
+
);
|
|
66379
|
+
}
|
|
66193
66380
|
if (this._persisted?.mlsGroups && this._state === "ready" && this._ws) {
|
|
66194
66381
|
const targetSharedGid = targetConvId ? this._sessionGroupIds?.get(targetConvId) : void 0;
|
|
66382
|
+
console.log(
|
|
66383
|
+
`[SecureChannel] send(): considering ${Object.keys(this._persisted.mlsGroups).length} shared group(s); targetConvId=${targetConvId ? targetConvId.slice(0, 8) : "(none \u2014 broadcast)"} targetSharedGid=${targetSharedGid ? targetSharedGid.slice(0, 8) : "(none)"}`
|
|
66384
|
+
);
|
|
66385
|
+
const skipped = [];
|
|
66195
66386
|
for (const [gid, entry] of Object.entries(this._persisted.mlsGroups)) {
|
|
66196
|
-
if (targetConvId && gid !== targetSharedGid)
|
|
66197
|
-
|
|
66387
|
+
if (targetConvId && gid !== targetSharedGid) {
|
|
66388
|
+
skipped.push(`${gid.slice(0, 8)}:not-target`);
|
|
66389
|
+
continue;
|
|
66390
|
+
}
|
|
66391
|
+
if (!entry.mlsGroupId) {
|
|
66392
|
+
skipped.push(`${gid.slice(0, 8)}:no-mlsGroupId`);
|
|
66393
|
+
continue;
|
|
66394
|
+
}
|
|
66198
66395
|
const mlsGroup = this._mlsGroups.get(`1to1-group:${gid}`);
|
|
66199
|
-
if (!mlsGroup?.isInitialized
|
|
66396
|
+
if (!mlsGroup?.isInitialized) {
|
|
66397
|
+
skipped.push(`${gid.slice(0, 8)}:not-in-memory`);
|
|
66398
|
+
continue;
|
|
66399
|
+
}
|
|
66400
|
+
if (Number(mlsGroup.epoch) <= 0) {
|
|
66401
|
+
skipped.push(`${gid.slice(0, 8)}:epoch<=0`);
|
|
66402
|
+
continue;
|
|
66403
|
+
}
|
|
66200
66404
|
try {
|
|
66201
66405
|
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
66202
|
-
const cipherBytes = await
|
|
66406
|
+
const cipherBytes = await this._encryptWithCatchUp(
|
|
66407
|
+
mlsGroup,
|
|
66408
|
+
`1to1-group:${gid}`,
|
|
66409
|
+
entry.mlsGroupId,
|
|
66410
|
+
`1:1 group ${gid.slice(0, 8)}`,
|
|
66411
|
+
plaintextBytes
|
|
66412
|
+
);
|
|
66203
66413
|
await saveMlsState(this.config.dataDir, entry.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
66204
66414
|
const payload = {
|
|
66205
66415
|
group_id: entry.mlsGroupId,
|
|
@@ -66221,8 +66431,12 @@ var init_channel = __esm2({
|
|
|
66221
66431
|
console.log(`[SecureChannel] Shared MLS group send for group ${gid.slice(0, 8)} (${entry.mlsGroupId.slice(0, 8)})`);
|
|
66222
66432
|
} catch (err) {
|
|
66223
66433
|
console.error(`[SecureChannel] Shared MLS group send failed for ${gid.slice(0, 8)}:`, err);
|
|
66434
|
+
skipped.push(`${gid.slice(0, 8)}:encrypt-threw`);
|
|
66224
66435
|
}
|
|
66225
66436
|
}
|
|
66437
|
+
if (skipped.length > 0) {
|
|
66438
|
+
console.log(`[SecureChannel] send(): skipped ${skipped.length} group(s) \u2014 ${skipped.join(", ")}`);
|
|
66439
|
+
}
|
|
66226
66440
|
}
|
|
66227
66441
|
for (const [convId, session] of this._sessions) {
|
|
66228
66442
|
if (!session.activated) continue;
|
|
@@ -66242,7 +66456,14 @@ var init_channel = __esm2({
|
|
|
66242
66456
|
}
|
|
66243
66457
|
if (mlsGroup?.isInitialized && mlsGroupId && Number(mlsGroup.epoch) > 0 && this._state === "ready" && this._ws) {
|
|
66244
66458
|
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
66245
|
-
const
|
|
66459
|
+
const sessionGroupKey = convGroupId && this._mlsGroups.has(`1to1-group:${convGroupId}`) ? `1to1-group:${convGroupId}` : `conv:${convId}`;
|
|
66460
|
+
const cipherBytes = await this._encryptWithCatchUp(
|
|
66461
|
+
mlsGroup,
|
|
66462
|
+
sessionGroupKey,
|
|
66463
|
+
mlsGroupId,
|
|
66464
|
+
`conversation ${convId.slice(0, 8)}`,
|
|
66465
|
+
plaintextBytes
|
|
66466
|
+
);
|
|
66246
66467
|
await saveMlsState(this.config.dataDir, mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
66247
66468
|
const payload = {
|
|
66248
66469
|
conversation_id: convId,
|
|
@@ -66337,7 +66558,14 @@ var init_channel = __esm2({
|
|
|
66337
66558
|
}
|
|
66338
66559
|
try {
|
|
66339
66560
|
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
66340
|
-
const
|
|
66561
|
+
const mlsOnlyGroupKey = mlsOnlyConvGroupId && this._mlsGroups.has(`1to1-group:${mlsOnlyConvGroupId}`) ? `1to1-group:${mlsOnlyConvGroupId}` : `conv:${mlsConvId}`;
|
|
66562
|
+
const cipherBytes = await this._encryptWithCatchUp(
|
|
66563
|
+
mlsGroup,
|
|
66564
|
+
mlsOnlyGroupKey,
|
|
66565
|
+
resolvedMlsGroupId,
|
|
66566
|
+
`conversation ${mlsConvId.slice(0, 8)}`,
|
|
66567
|
+
plaintextBytes
|
|
66568
|
+
);
|
|
66341
66569
|
await saveMlsState(this.config.dataDir, resolvedMlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
66342
66570
|
const payload = {
|
|
66343
66571
|
conversation_id: mlsConvId,
|
|
@@ -66363,13 +66591,26 @@ var init_channel = __esm2({
|
|
|
66363
66591
|
}
|
|
66364
66592
|
}
|
|
66365
66593
|
}
|
|
66366
|
-
if (sentCount === 0
|
|
66367
|
-
console.warn(
|
|
66594
|
+
if (sentCount === 0) {
|
|
66595
|
+
console.warn(
|
|
66596
|
+
`[SecureChannel] send() delivered to 0 destinations \u2014 nothing was encrypted or queued (sessions=${this._sessions.size}, sharedGroups=${this._persisted?.mlsGroups ? Object.keys(this._persisted.mlsGroups).length : 0})`
|
|
66597
|
+
);
|
|
66368
66598
|
}
|
|
66369
66599
|
await this._persistState();
|
|
66600
|
+
const wsReady = this._ws?.readyState === 1;
|
|
66601
|
+
if (pendingWsSends.length > 0 && !wsReady) {
|
|
66602
|
+
console.warn(
|
|
66603
|
+
`[SecureChannel] send(): ${pendingWsSends.length} frame(s) NOT written \u2014 socket readyState=${this._ws?.readyState ?? "null"} (1=OPEN)`
|
|
66604
|
+
);
|
|
66605
|
+
}
|
|
66370
66606
|
for (const frame of pendingWsSends) {
|
|
66371
66607
|
this._ws.send(frame);
|
|
66372
66608
|
}
|
|
66609
|
+
if (pendingWsSends.length > 0) {
|
|
66610
|
+
console.log(
|
|
66611
|
+
`[SecureChannel] send(): wrote ${pendingWsSends.length} frame(s) to the socket (sentCount=${sentCount}, readyState=${this._ws?.readyState ?? "null"})`
|
|
66612
|
+
);
|
|
66613
|
+
}
|
|
66373
66614
|
if (!options?.isResend) {
|
|
66374
66615
|
for (const mlsGroupId of addressedMlsGroupIds) {
|
|
66375
66616
|
this._rememberRetryCandidate(mlsGroupId, plaintext, options);
|
|
@@ -66419,7 +66660,7 @@ var init_channel = __esm2({
|
|
|
66419
66660
|
*/
|
|
66420
66661
|
sendActivitySpan(spanData) {
|
|
66421
66662
|
if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;
|
|
66422
|
-
const pluginVersion = true ? "0.23.
|
|
66663
|
+
const pluginVersion = true ? "0.23.29" : "0.0.0-dev";
|
|
66423
66664
|
const agentName = this.config.agentName ?? "Agent";
|
|
66424
66665
|
const resource = {
|
|
66425
66666
|
"service.name": "agentvault-agent",
|
|
@@ -66613,6 +66854,7 @@ var init_channel = __esm2({
|
|
|
66613
66854
|
* Uses MLS (preferred) or pairwise fan-out as fallback.
|
|
66614
66855
|
*/
|
|
66615
66856
|
async sendToRoom(roomId, plaintext, opts) {
|
|
66857
|
+
if (isAbstention(plaintext)) return;
|
|
66616
66858
|
if (!this._persisted?.rooms?.[roomId]) {
|
|
66617
66859
|
throw new Error(`Room ${roomId} not found`);
|
|
66618
66860
|
}
|
|
@@ -66629,7 +66871,13 @@ var init_channel = __esm2({
|
|
|
66629
66871
|
if (mlsGroup?.isInitialized) {
|
|
66630
66872
|
try {
|
|
66631
66873
|
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
66632
|
-
const ciphertext = await
|
|
66874
|
+
const ciphertext = await this._encryptWithCatchUp(
|
|
66875
|
+
mlsGroup,
|
|
66876
|
+
roomId,
|
|
66877
|
+
room.mlsGroupId,
|
|
66878
|
+
`room ${roomId.slice(0, 8)}`,
|
|
66879
|
+
plaintextBytes
|
|
66880
|
+
);
|
|
66633
66881
|
await saveMlsState(this.config.dataDir, room.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
66634
66882
|
if (this._state === "ready" && this._ws) {
|
|
66635
66883
|
this._ws.send(JSON.stringify({
|
|
@@ -66880,11 +67128,17 @@ var init_channel = __esm2({
|
|
|
66880
67128
|
const before = this._persisted.groupId;
|
|
66881
67129
|
this._sessionGroupIds = new Map(mine.map((r2) => [r2.id, r2.group_id]));
|
|
66882
67130
|
this._persisted.conversationGroupIds = Object.fromEntries(this._sessionGroupIds);
|
|
66883
|
-
const
|
|
67131
|
+
const joined = mine.find((r2) => this._persisted.mlsGroups?.[r2.group_id]);
|
|
67132
|
+
const primary = mine.find((r2) => r2.id === this._persisted.primaryConversationId) ?? joined ?? mine[0];
|
|
67133
|
+
if (!mine.some((r2) => r2.id === this._persisted.primaryConversationId)) {
|
|
67134
|
+
console.log(
|
|
67135
|
+
`[SecureChannel] Adopting new primary conversation ${primary.id.slice(0, 8)} (group ${primary.group_id.slice(0, 8)}) \u2014 ${joined ? "we hold MLS state for it" : "no joined conversation found, first active row"}`
|
|
67136
|
+
);
|
|
67137
|
+
}
|
|
66884
67138
|
this._persisted.groupId = primary.group_id;
|
|
66885
67139
|
this._persisted.primaryConversationId = primary.id;
|
|
66886
67140
|
const liveGroupIds = new Set(mine.map((r2) => r2.group_id));
|
|
66887
|
-
if (!liveGroupIds.has(staleGid)) {
|
|
67141
|
+
if (staleGid && staleMlsGroupId && !liveGroupIds.has(staleGid)) {
|
|
66888
67142
|
try {
|
|
66889
67143
|
await deleteMlsState(this.config.dataDir, staleMlsGroupId);
|
|
66890
67144
|
} catch {
|
|
@@ -66898,9 +67152,9 @@ var init_channel = __esm2({
|
|
|
66898
67152
|
}
|
|
66899
67153
|
await this._persistState();
|
|
66900
67154
|
console.log(
|
|
66901
|
-
`[SecureChannel] Re-resolved conversation groups after 'unknown group' ${staleMlsGroupId.slice(0, 8)}: primary group ${String(before).slice(0, 8)} \u2192 ${this._persisted.groupId.slice(0, 8)} (${mine.length} active conversation(s))`
|
|
67155
|
+
`[SecureChannel] Re-resolved conversation groups ${staleMlsGroupId ? `after 'unknown group' ${staleMlsGroupId.slice(0, 8)}` : "on connect (#1034)"}: primary group ${String(before).slice(0, 8)} \u2192 ${this._persisted.groupId.slice(0, 8)} (${mine.length} active conversation(s))`
|
|
66902
67156
|
);
|
|
66903
|
-
return this._persisted.groupId !== before && before === staleGid;
|
|
67157
|
+
return staleGid !== void 0 && this._persisted.groupId !== before && before === staleGid;
|
|
66904
67158
|
}
|
|
66905
67159
|
/**
|
|
66906
67160
|
* Resend the message that a now-reconciled `unknown group` refusal killed (#732).
|
|
@@ -67808,7 +68062,13 @@ var init_channel = __esm2({
|
|
|
67808
68062
|
const mlsGroup = this._mlsGroups.get(`a2a:${channelEntry.channelId}`);
|
|
67809
68063
|
if (mlsGroup?.isInitialized) {
|
|
67810
68064
|
const plaintextBytes = new TextEncoder().encode(text);
|
|
67811
|
-
const ciphertext = await
|
|
68065
|
+
const ciphertext = await this._encryptWithCatchUp(
|
|
68066
|
+
mlsGroup,
|
|
68067
|
+
`a2a:${channelEntry.channelId}`,
|
|
68068
|
+
channelEntry.mlsGroupId,
|
|
68069
|
+
`A2A ${channelEntry.channelId.slice(0, 8)}`,
|
|
68070
|
+
plaintextBytes
|
|
68071
|
+
);
|
|
67812
68072
|
await saveMlsState(
|
|
67813
68073
|
this.config.dataDir,
|
|
67814
68074
|
channelEntry.mlsGroupId,
|
|
@@ -67856,7 +68116,13 @@ var init_channel = __esm2({
|
|
|
67856
68116
|
const mlsGroup = this._mlsGroups.get(`a2a:${channelId}`);
|
|
67857
68117
|
if (mlsGroup?.isInitialized) {
|
|
67858
68118
|
const plaintextBytes = new TextEncoder().encode(text);
|
|
67859
|
-
const ciphertext = await
|
|
68119
|
+
const ciphertext = await this._encryptWithCatchUp(
|
|
68120
|
+
mlsGroup,
|
|
68121
|
+
`a2a:${channelId}`,
|
|
68122
|
+
entry.mlsGroupId,
|
|
68123
|
+
`A2A ${channelId.slice(0, 8)}`,
|
|
68124
|
+
plaintextBytes
|
|
68125
|
+
);
|
|
67860
68126
|
await saveMlsState(this.config.dataDir, entry.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
67861
68127
|
this._ws.send(JSON.stringify({
|
|
67862
68128
|
event: "a2a_message_mls",
|
|
@@ -68302,6 +68568,9 @@ var init_channel = __esm2({
|
|
|
68302
68568
|
await this._pullDrDeliveryQueue();
|
|
68303
68569
|
await this._flushOutboundQueue();
|
|
68304
68570
|
this._setState("ready");
|
|
68571
|
+
void this._reconcileConversationGroups().catch((err) => {
|
|
68572
|
+
console.warn("[SecureChannel] Conversation-group reconcile failed:", err);
|
|
68573
|
+
});
|
|
68305
68574
|
void this._reconcileRoomsWithServer().catch(
|
|
68306
68575
|
(err) => console.warn(`[SecureChannel] room reconcile failed (ignored): ${err instanceof Error ? err.message : String(err)}`)
|
|
68307
68576
|
).then(() => this._quarantineOrphanedMlsGroups()).catch(
|
|
@@ -68329,7 +68598,7 @@ var init_channel = __esm2({
|
|
|
68329
68598
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
68330
68599
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
68331
68600
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
68332
|
-
pluginVersion: true ? "0.23.
|
|
68601
|
+
pluginVersion: true ? "0.23.29" : "0.0.0-dev"
|
|
68333
68602
|
});
|
|
68334
68603
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
68335
68604
|
}
|
|
@@ -68653,7 +68922,7 @@ var init_channel = __esm2({
|
|
|
68653
68922
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
68654
68923
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
68655
68924
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
68656
|
-
pluginVersion: true ? "0.23.
|
|
68925
|
+
pluginVersion: true ? "0.23.29" : "0.0.0-dev"
|
|
68657
68926
|
});
|
|
68658
68927
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
68659
68928
|
}
|
|
@@ -69019,6 +69288,7 @@ var init_channel = __esm2({
|
|
|
69019
69288
|
const convGroupId = data.conversation_group_id;
|
|
69020
69289
|
const groupId = data.group_id;
|
|
69021
69290
|
const senderDeviceId = data.sender_device_id;
|
|
69291
|
+
this._noteGroupEpoch(groupId, data.epoch);
|
|
69022
69292
|
if (senderDeviceId === this._deviceId) return;
|
|
69023
69293
|
let mgr;
|
|
69024
69294
|
let mgrKey;
|
|
@@ -69096,9 +69366,15 @@ var init_channel = __esm2({
|
|
|
69096
69366
|
})),
|
|
69097
69367
|
no_history: filtered.length === 0
|
|
69098
69368
|
});
|
|
69099
|
-
if (mgr && mlsGroupId) {
|
|
69369
|
+
if (mgr && mgrKey && mlsGroupId) {
|
|
69100
69370
|
const responseBytes = new TextEncoder().encode(responsePayload);
|
|
69101
|
-
const cipher = await
|
|
69371
|
+
const cipher = await this._encryptWithCatchUp(
|
|
69372
|
+
mgr,
|
|
69373
|
+
mgrKey,
|
|
69374
|
+
mlsGroupId,
|
|
69375
|
+
`history catch-up ${mlsGroupId.slice(0, 8)}`,
|
|
69376
|
+
responseBytes
|
|
69377
|
+
);
|
|
69102
69378
|
await saveMlsState(this.config.dataDir, mlsGroupId, JSON.stringify(mgr.exportState()));
|
|
69103
69379
|
if (this._ws) {
|
|
69104
69380
|
this._ws.send(JSON.stringify({
|
|
@@ -69841,9 +70117,36 @@ ${messageText}`;
|
|
|
69841
70117
|
// ---------------------------------------------------------------------------
|
|
69842
70118
|
// MLS room message handlers
|
|
69843
70119
|
// ---------------------------------------------------------------------------
|
|
70120
|
+
/**
|
|
70121
|
+
* #1014: hand a room-framed frame to the handler that owns its group family.
|
|
70122
|
+
*
|
|
70123
|
+
* Returns true when the frame was routed. Routing means "give it to the right
|
|
70124
|
+
* handler" — NOT "decrypt it as a room message". Decrypting under the wrong
|
|
70125
|
+
* group is how MLS state gets corrupted, so an unplaceable group is left for
|
|
70126
|
+
* the caller to self-heal rather than guessed at.
|
|
70127
|
+
*/
|
|
70128
|
+
async _routeNonRoomGroupMessage(data, groupId) {
|
|
70129
|
+
const target = this._resolveGroupFamily(groupId);
|
|
70130
|
+
if (!target || target.family === "room") return false;
|
|
70131
|
+
console.log(
|
|
70132
|
+
`[SecureChannel] Room-framed message belongs to ${target.label} \u2014 routing to the handler that owns it (#1014)`
|
|
70133
|
+
);
|
|
70134
|
+
switch (target.family) {
|
|
70135
|
+
case "shared1to1":
|
|
70136
|
+
await this._handleMessageMLS({ ...data, conversation_group_id: target.id });
|
|
70137
|
+
return true;
|
|
70138
|
+
case "conversation":
|
|
70139
|
+
await this._handleMessageMLS({ ...data, conversation_id: target.id });
|
|
70140
|
+
return true;
|
|
70141
|
+
case "a2a":
|
|
70142
|
+
await this._handleA2AMessageMLS({ ...data, a2a_channel_id: target.id });
|
|
70143
|
+
return true;
|
|
70144
|
+
}
|
|
70145
|
+
}
|
|
69844
70146
|
async _handleRoomMessageMLS(data) {
|
|
69845
70147
|
const groupId = data.group_id;
|
|
69846
70148
|
const senderDeviceId = data.sender_device_id;
|
|
70149
|
+
this._noteGroupEpoch(groupId, data.epoch);
|
|
69847
70150
|
if (senderDeviceId === this._deviceId) return;
|
|
69848
70151
|
let roomId;
|
|
69849
70152
|
for (const [rid, room] of Object.entries(this._persisted?.rooms ?? {})) {
|
|
@@ -69857,7 +70160,9 @@ ${messageText}`;
|
|
|
69857
70160
|
roomId = data.room_id;
|
|
69858
70161
|
console.log(`[SecureChannel] Room ${roomId.slice(0, 8)} matched by room_id (group_id ${groupId?.slice(0, 8)} mismatch)`);
|
|
69859
70162
|
} else {
|
|
70163
|
+
if (await this._routeNonRoomGroupMessage(data, groupId)) return;
|
|
69860
70164
|
console.warn(`[SecureChannel] No room found for MLS group ${groupId?.slice(0, 8)}`);
|
|
70165
|
+
if (groupId) void this._requestWelcomeSelfHeal(groupId);
|
|
69861
70166
|
return;
|
|
69862
70167
|
}
|
|
69863
70168
|
}
|
|
@@ -70045,63 +70350,63 @@ ${messageText}`;
|
|
|
70045
70350
|
console.warn(`[SecureChannel] Failed to send history catchup response:`, sendErr);
|
|
70046
70351
|
}
|
|
70047
70352
|
}
|
|
70353
|
+
/**
|
|
70354
|
+
* Apply an inbound MLS commit to whichever group it addresses.
|
|
70355
|
+
*
|
|
70356
|
+
* Returns TRUE when the commit was applied, buffered, or handed to the
|
|
70357
|
+
* failure ladder — i.e. when this agent has taken responsibility for it and
|
|
70358
|
+
* the delivery-queue row may safely be acked. Returns FALSE when the commit
|
|
70359
|
+
* names a group we hold no state for at all, so the caller must NACK and let
|
|
70360
|
+
* the row survive.
|
|
70361
|
+
*
|
|
70362
|
+
* #1012: this used to loop `rooms` then `a2aChannels` and then END. A shared
|
|
70363
|
+
* 1:1 commit (`mlsGroups`) and a legacy per-conversation commit
|
|
70364
|
+
* (`mlsConversations`) matched neither. The call fell off the bottom with no
|
|
70365
|
+
* log, no buffer and no error — and because it did not THROW,
|
|
70366
|
+
* `_pullDeliveryQueue` acked the row and the server deleted it. An MLS commit
|
|
70367
|
+
* cannot be regenerated, so the agent stayed at epoch N while the group moved
|
|
70368
|
+
* to N+1 and every later send() encrypted at an epoch nobody could decrypt.
|
|
70369
|
+
*
|
|
70370
|
+
* The families are enumerated in ONE list resolved by ONE loop, because two
|
|
70371
|
+
* hand-copied loops are exactly how the 1:1 path fell two families behind.
|
|
70372
|
+
*/
|
|
70048
70373
|
async _handleMlsCommit(data) {
|
|
70049
70374
|
const groupId = data.group_id;
|
|
70050
70375
|
const epoch = typeof data.epoch === "number" ? data.epoch : 0;
|
|
70051
|
-
|
|
70052
|
-
|
|
70053
|
-
|
|
70054
|
-
|
|
70055
|
-
|
|
70056
|
-
|
|
70057
|
-
|
|
70058
|
-
|
|
70059
|
-
|
|
70060
|
-
this._mlsCommitFailCounts.delete(roomId);
|
|
70061
|
-
} catch (err) {
|
|
70062
|
-
await this._onCommitFailure(
|
|
70063
|
-
roomId,
|
|
70064
|
-
groupId,
|
|
70065
|
-
err,
|
|
70066
|
-
`room ${roomId.slice(0, 8)}`,
|
|
70067
|
-
data.epoch,
|
|
70068
|
-
mlsGroup.epoch
|
|
70069
|
-
);
|
|
70070
|
-
}
|
|
70071
|
-
} else {
|
|
70072
|
-
this._bufferMlsCommit(groupId, epoch, data);
|
|
70073
|
-
console.log(`[SecureChannel] Buffered MLS commit for room ${roomId.slice(0, 8)} (epoch=${epoch}, group not initialized)`);
|
|
70074
|
-
}
|
|
70075
|
-
return;
|
|
70376
|
+
this._noteGroupEpoch(groupId, data.epoch);
|
|
70377
|
+
const target = this._resolveGroupFamily(groupId);
|
|
70378
|
+
if (target) {
|
|
70379
|
+
const { managerKey: groupKey, label } = target;
|
|
70380
|
+
const mlsGroup = this._mlsGroups.get(groupKey);
|
|
70381
|
+
if (!mlsGroup?.isInitialized) {
|
|
70382
|
+
this._bufferMlsCommit(groupId, epoch, data);
|
|
70383
|
+
console.log(`[SecureChannel] Buffered MLS commit for ${label} (epoch=${epoch}, group not initialized)`);
|
|
70384
|
+
return true;
|
|
70076
70385
|
}
|
|
70077
|
-
|
|
70078
|
-
|
|
70079
|
-
|
|
70080
|
-
|
|
70081
|
-
|
|
70082
|
-
|
|
70083
|
-
|
|
70084
|
-
|
|
70085
|
-
|
|
70086
|
-
|
|
70087
|
-
|
|
70088
|
-
|
|
70089
|
-
|
|
70090
|
-
|
|
70091
|
-
|
|
70092
|
-
err,
|
|
70093
|
-
`A2A ${chId.slice(0, 8)}`,
|
|
70094
|
-
data.epoch,
|
|
70095
|
-
mlsGroup.epoch
|
|
70096
|
-
);
|
|
70097
|
-
}
|
|
70098
|
-
} else {
|
|
70099
|
-
this._bufferMlsCommit(groupId, epoch, data);
|
|
70100
|
-
console.log(`[SecureChannel] Buffered MLS commit for A2A ${chId.slice(0, 8)} (epoch=${epoch}, group not initialized)`);
|
|
70101
|
-
}
|
|
70102
|
-
return;
|
|
70386
|
+
try {
|
|
70387
|
+
const commitBytes = new Uint8Array(Buffer.from(data.payload, "hex"));
|
|
70388
|
+
await mlsGroup.processCommit(commitBytes);
|
|
70389
|
+
await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mlsGroup.exportState()));
|
|
70390
|
+
console.log(`[SecureChannel] MLS commit processed for ${label} (epoch=${mlsGroup.epoch})`);
|
|
70391
|
+
this._mlsCommitFailCounts.delete(groupKey);
|
|
70392
|
+
} catch (err) {
|
|
70393
|
+
await this._onCommitFailure(
|
|
70394
|
+
groupKey,
|
|
70395
|
+
groupId,
|
|
70396
|
+
err,
|
|
70397
|
+
label,
|
|
70398
|
+
data.epoch,
|
|
70399
|
+
mlsGroup.epoch
|
|
70400
|
+
);
|
|
70103
70401
|
}
|
|
70402
|
+
return true;
|
|
70104
70403
|
}
|
|
70404
|
+
this._bufferMlsCommit(groupId, epoch, data);
|
|
70405
|
+
console.warn(
|
|
70406
|
+
`[SecureChannel] MLS commit for UNKNOWN group ${String(groupId).slice(0, 8)} (epoch=${epoch}) \u2014 buffered, requesting a Welcome, NOT acked`
|
|
70407
|
+
);
|
|
70408
|
+
if (groupId) void this._requestWelcomeSelfHeal(groupId);
|
|
70409
|
+
return false;
|
|
70105
70410
|
}
|
|
70106
70411
|
/** Buffer an MLS commit for replay after Welcome join. Max 50 per group. */
|
|
70107
70412
|
_bufferMlsCommit(groupId, epoch, data) {
|
|
@@ -70291,6 +70596,229 @@ ${messageText}`;
|
|
|
70291
70596
|
* `1to1-group:<gid>`, `a2a:<channelId>`, …) — also the decrypt-fail-count key.
|
|
70292
70597
|
* @param groupId the server-side MLS group id used in the /sync URL.
|
|
70293
70598
|
*/
|
|
70599
|
+
/**
|
|
70600
|
+
* #1015 — the ONLY place this package fulfils a Welcome request.
|
|
70601
|
+
*
|
|
70602
|
+
* Add the members, transmit, and record `fulfilled` **only for the targets
|
|
70603
|
+
* whose frames actually went out**. Every one of the five sites that used to
|
|
70604
|
+
* do this by hand followed the same sequence and had the same three holes:
|
|
70605
|
+
* `this._ws` null (both sends skipped by an `if`), a CLOSING socket (`send`
|
|
70606
|
+
* returns without throwing), and a fulfil POST wrapped in `.catch(() => {})`.
|
|
70607
|
+
*
|
|
70608
|
+
* A fulfilled request is never re-requested. So a Welcome that evaporated
|
|
70609
|
+
* left the requester locked out, the fulfiller at epoch N+1, everyone else at
|
|
70610
|
+
* N, and the bridging commit untransmitted and impossible to regenerate — a
|
|
70611
|
+
* permanently wedged group in which every signal read success.
|
|
70612
|
+
*
|
|
70613
|
+
* Returns the number of requests actually fulfilled.
|
|
70614
|
+
*/
|
|
70615
|
+
async _fulfilWelcomeRequests(opts) {
|
|
70616
|
+
const { mlsGroup, mlsGroupId, label, welcomeFields, targets } = opts;
|
|
70617
|
+
if (targets.length === 0) return 0;
|
|
70618
|
+
if (!this._wsIsOpen()) {
|
|
70619
|
+
console.warn(
|
|
70620
|
+
`[SecureChannel] ${label}: socket is not OPEN \u2014 NOT advancing the group; ${targets.length} request(s) stay pending for the next pull`
|
|
70621
|
+
);
|
|
70622
|
+
return 0;
|
|
70623
|
+
}
|
|
70624
|
+
const snapshot = mlsGroup.exportState();
|
|
70625
|
+
const { commit, welcome } = await mlsGroup.addMembers(targets.map((t2) => t2.keyPackage));
|
|
70626
|
+
const commitSent = await this._sendFrameConfirmed(
|
|
70627
|
+
{
|
|
70628
|
+
event: "mls_commit",
|
|
70629
|
+
data: {
|
|
70630
|
+
group_id: mlsGroupId,
|
|
70631
|
+
epoch: Number(mlsGroup.epoch),
|
|
70632
|
+
payload: Buffer.from(commit).toString("hex")
|
|
70633
|
+
}
|
|
70634
|
+
},
|
|
70635
|
+
`${label} commit`
|
|
70636
|
+
);
|
|
70637
|
+
if (!commitSent) {
|
|
70638
|
+
mlsGroup.importState(snapshot);
|
|
70639
|
+
console.warn(
|
|
70640
|
+
`[SecureChannel] ${label}: commit was NOT written \u2014 rolled back to epoch ${Number(mlsGroup.epoch)}, nothing fulfilled, requests stay pending`
|
|
70641
|
+
);
|
|
70642
|
+
return 0;
|
|
70643
|
+
}
|
|
70644
|
+
await saveMlsState(this.config.dataDir, mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
70645
|
+
if (!welcome) {
|
|
70646
|
+
console.warn(
|
|
70647
|
+
`[SecureChannel] ${label}: addMembers produced NO Welcome \u2014 ${targets.length} request(s) left pending rather than marked fulfilled`
|
|
70648
|
+
);
|
|
70649
|
+
return 0;
|
|
70650
|
+
}
|
|
70651
|
+
let fulfilled = 0;
|
|
70652
|
+
for (const t2 of targets) {
|
|
70653
|
+
const sent = await this._sendFrameConfirmed(
|
|
70654
|
+
{
|
|
70655
|
+
event: "mls_welcome",
|
|
70656
|
+
data: {
|
|
70657
|
+
target_device_id: t2.deviceId,
|
|
70658
|
+
group_id: mlsGroupId,
|
|
70659
|
+
...welcomeFields,
|
|
70660
|
+
payload: Buffer.from(welcome).toString("hex")
|
|
70661
|
+
}
|
|
70662
|
+
},
|
|
70663
|
+
`${label} welcome\u2192${t2.deviceId.slice(0, 8)}`
|
|
70664
|
+
);
|
|
70665
|
+
if (!sent) {
|
|
70666
|
+
console.warn(
|
|
70667
|
+
`[SecureChannel] ${label}: Welcome for ${t2.deviceId.slice(0, 8)} was NOT written \u2014 leaving the request PENDING so the next pull retries it`
|
|
70668
|
+
);
|
|
70669
|
+
continue;
|
|
70670
|
+
}
|
|
70671
|
+
if (await this._postFulfilWelcome(mlsGroupId, t2.requestId, `${label} ${t2.deviceId.slice(0, 8)}`)) {
|
|
70672
|
+
fulfilled++;
|
|
70673
|
+
}
|
|
70674
|
+
}
|
|
70675
|
+
return fulfilled;
|
|
70676
|
+
}
|
|
70677
|
+
/**
|
|
70678
|
+
* Resolve an MLS group id to the family that owns it.
|
|
70679
|
+
*
|
|
70680
|
+
* There are four: rooms, A2A channels, shared 1:1 groups, and the legacy
|
|
70681
|
+
* per-conversation groups. They all store the MLS group id under
|
|
70682
|
+
* `mlsGroupId`; they differ only in the key their live `MLSGroupManager` sits
|
|
70683
|
+
* under in `_mlsGroups`.
|
|
70684
|
+
*
|
|
70685
|
+
* ONE resolver, because hand-copying this enumeration is what produced
|
|
70686
|
+
* #1012 (`_handleMlsCommit` handled two families of four), #1014 (a
|
|
70687
|
+
* room-framed 1:1 replay matched nothing) and #1016 (a rejoin dropped only
|
|
70688
|
+
* the room manager). Three bugs, one missing abstraction.
|
|
70689
|
+
*/
|
|
70690
|
+
_resolveGroupFamily(mlsGroupId) {
|
|
70691
|
+
if (!mlsGroupId) return null;
|
|
70692
|
+
const families = [
|
|
70693
|
+
{ family: "room", entries: this._persisted?.rooms, managerKey: (id) => id, label: "room" },
|
|
70694
|
+
{ family: "a2a", entries: this._persisted?.a2aChannels, managerKey: (id) => `a2a:${id}`, label: "A2A" },
|
|
70695
|
+
{ family: "shared1to1", entries: this._persisted?.mlsGroups, managerKey: (id) => `1to1-group:${id}`, label: "1:1 group" },
|
|
70696
|
+
{ family: "conversation", entries: this._persisted?.mlsConversations, managerKey: (id) => `conv:${id}`, label: "conversation" }
|
|
70697
|
+
];
|
|
70698
|
+
for (const f2 of families) {
|
|
70699
|
+
for (const [id, entry] of Object.entries(f2.entries ?? {})) {
|
|
70700
|
+
if (entry?.mlsGroupId !== mlsGroupId) continue;
|
|
70701
|
+
return {
|
|
70702
|
+
family: f2.family,
|
|
70703
|
+
id,
|
|
70704
|
+
managerKey: f2.managerKey(id),
|
|
70705
|
+
label: `${f2.label} ${id.slice(0, 8)}`
|
|
70706
|
+
};
|
|
70707
|
+
}
|
|
70708
|
+
}
|
|
70709
|
+
return null;
|
|
70710
|
+
}
|
|
70711
|
+
/** #1015: is the socket in a state that can actually take a write? */
|
|
70712
|
+
_wsIsOpen() {
|
|
70713
|
+
return this._ws?.readyState === 1;
|
|
70714
|
+
}
|
|
70715
|
+
/**
|
|
70716
|
+
* #1015: write one frame and report whether the socket took it.
|
|
70717
|
+
*
|
|
70718
|
+
* `ws.send()` on a CLOSING or CLOSED socket returns WITHOUT throwing — the
|
|
70719
|
+
* error surfaces only through the callback, and every fulfilment site passed
|
|
70720
|
+
* none. So a Welcome could be "sent" into a dead socket and the requester
|
|
70721
|
+
* still marked `fulfilled`, which it never re-requests.
|
|
70722
|
+
*/
|
|
70723
|
+
_sendFrameConfirmed(frame, label) {
|
|
70724
|
+
const ws = this._ws;
|
|
70725
|
+
if (!ws || ws.readyState !== 1) {
|
|
70726
|
+
console.warn(
|
|
70727
|
+
`[SecureChannel] ${label}: socket is not OPEN (readyState=${ws?.readyState ?? "null"}) \u2014 frame NOT written`
|
|
70728
|
+
);
|
|
70729
|
+
return Promise.resolve(false);
|
|
70730
|
+
}
|
|
70731
|
+
return new Promise((resolve3) => {
|
|
70732
|
+
try {
|
|
70733
|
+
ws.send(JSON.stringify(frame), (err) => {
|
|
70734
|
+
if (err) {
|
|
70735
|
+
console.error(`[SecureChannel] ${label}: socket refused the frame:`, err);
|
|
70736
|
+
resolve3(false);
|
|
70737
|
+
} else {
|
|
70738
|
+
resolve3(true);
|
|
70739
|
+
}
|
|
70740
|
+
});
|
|
70741
|
+
} catch (err) {
|
|
70742
|
+
console.error(`[SecureChannel] ${label}: send threw:`, err);
|
|
70743
|
+
resolve3(false);
|
|
70744
|
+
}
|
|
70745
|
+
});
|
|
70746
|
+
}
|
|
70747
|
+
/**
|
|
70748
|
+
* #1015: record a Welcome as fulfilled — and say so when it fails.
|
|
70749
|
+
*
|
|
70750
|
+
* The batched shared-1:1 site posted this with `.catch(() => {})`, so an HTTP
|
|
70751
|
+
* failure here was invisible on top of the sends being invisible.
|
|
70752
|
+
*/
|
|
70753
|
+
async _postFulfilWelcome(mlsGroupId, requestId, label) {
|
|
70754
|
+
try {
|
|
70755
|
+
const res = await fetch(
|
|
70756
|
+
`${this.config.apiUrl}/api/v1/mls/groups/${mlsGroupId}/fulfill-welcome`,
|
|
70757
|
+
{
|
|
70758
|
+
method: "POST",
|
|
70759
|
+
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
70760
|
+
body: JSON.stringify({ request_id: requestId })
|
|
70761
|
+
}
|
|
70762
|
+
);
|
|
70763
|
+
if (!res.ok) {
|
|
70764
|
+
console.warn(`[SecureChannel] ${label}: fulfill-welcome returned ${res.status}`);
|
|
70765
|
+
return false;
|
|
70766
|
+
}
|
|
70767
|
+
return true;
|
|
70768
|
+
} catch (err) {
|
|
70769
|
+
console.warn(`[SecureChannel] ${label}: fulfill-welcome failed:`, err);
|
|
70770
|
+
return false;
|
|
70771
|
+
}
|
|
70772
|
+
}
|
|
70773
|
+
/**
|
|
70774
|
+
* #1013: fold an epoch seen on an inbound frame into the running hint.
|
|
70775
|
+
*
|
|
70776
|
+
* Anything unusable (missing, non-numeric, negative, fractional) is ignored
|
|
70777
|
+
* rather than defaulted. A defaulted epoch here would be a guess, and a guess
|
|
70778
|
+
* that reads LOW silently disables the check it exists to drive.
|
|
70779
|
+
*/
|
|
70780
|
+
_noteGroupEpoch(mlsGroupId, epoch) {
|
|
70781
|
+
if (typeof mlsGroupId !== "string" || !mlsGroupId) return;
|
|
70782
|
+
const current = this._observedGroupEpochs.get(mlsGroupId) ?? 0;
|
|
70783
|
+
const next = noteObservedEpoch(current, epoch);
|
|
70784
|
+
if (next > current) this._observedGroupEpochs.set(mlsGroupId, next);
|
|
70785
|
+
}
|
|
70786
|
+
/**
|
|
70787
|
+
* #1013 — the ONLY place this package encrypts an MLS application message.
|
|
70788
|
+
*
|
|
70789
|
+
* Catch up first when the group is known to have moved past us, then encrypt.
|
|
70790
|
+
* Hermes has done this since #440 and the browser since #996; the plugin —
|
|
70791
|
+
* the implementation that actually ships to customers — did not, so an agent
|
|
70792
|
+
* that missed one commit encrypted every later message at an epoch nobody
|
|
70793
|
+
* could read, while the server stored and relayed it and every signal read
|
|
70794
|
+
* 200.
|
|
70795
|
+
*
|
|
70796
|
+
* Funnelling every sender through one method is the point. A guard applied at
|
|
70797
|
+
* seven call sites decays to a guard applied at six: that is exactly how
|
|
70798
|
+
* `_handleMlsCommit` ended up handling two group families out of four (#1012).
|
|
70799
|
+
*
|
|
70800
|
+
* Best-effort by design. A catch-up that cannot replay — or that throws — must
|
|
70801
|
+
* NOT block the send; we fall through and encrypt at the local epoch, which is
|
|
70802
|
+
* precisely today's behaviour, so the worst case is no regression.
|
|
70803
|
+
*/
|
|
70804
|
+
async _encryptWithCatchUp(mlsGroup, groupKey, mlsGroupId, label, plaintextBytes) {
|
|
70805
|
+
const observed = this._observedGroupEpochs.get(mlsGroupId);
|
|
70806
|
+
if (observed !== void 0 && needsPreSendResync(Number(mlsGroup.epoch), observed)) {
|
|
70807
|
+
console.warn(
|
|
70808
|
+
`[SecureChannel] ${label}: local epoch ${Number(mlsGroup.epoch)} is behind the observed group epoch ${observed} \u2014 catching up before encrypt (#1013)`
|
|
70809
|
+
);
|
|
70810
|
+
try {
|
|
70811
|
+
if (!await this._resyncOnDivergence(groupKey, mlsGroupId)) {
|
|
70812
|
+
console.warn(
|
|
70813
|
+
`[SecureChannel] ${label}: catch-up could not replay \u2014 sending at epoch ${Number(mlsGroup.epoch)}. Peers past this epoch will not read it.`
|
|
70814
|
+
);
|
|
70815
|
+
}
|
|
70816
|
+
} catch (err) {
|
|
70817
|
+
console.warn(`[SecureChannel] ${label}: catch-up failed:`, err);
|
|
70818
|
+
}
|
|
70819
|
+
}
|
|
70820
|
+
return await mlsGroup.encrypt(plaintextBytes);
|
|
70821
|
+
}
|
|
70294
70822
|
async _resyncOnDivergence(groupKey, groupId) {
|
|
70295
70823
|
const mgr = this._mlsGroups.get(groupKey);
|
|
70296
70824
|
if (!mgr?.isInitialized) return false;
|
|
@@ -70317,11 +70845,24 @@ ${messageText}`;
|
|
|
70317
70845
|
applied++;
|
|
70318
70846
|
}
|
|
70319
70847
|
if (applied === 0) {
|
|
70320
|
-
|
|
70848
|
+
const strikes = (this._resyncNoOpStrikes.get(groupKey) ?? 0) + 1;
|
|
70849
|
+
this._resyncNoOpStrikes.set(groupKey, strikes);
|
|
70850
|
+
if (strikes < _SecureChannel.MAX_RESYNC_NOOPS) {
|
|
70851
|
+
this._mlsDecryptFailCounts.delete(groupKey);
|
|
70852
|
+
console.log(
|
|
70853
|
+
`[SecureChannel] Resync for ${groupId.slice(0, 8)}: already current at epoch=${mgr.epoch} (strike ${strikes}/${_SecureChannel.MAX_RESYNC_NOOPS}) \u2014 NOT re-keying (#1084)`
|
|
70854
|
+
);
|
|
70855
|
+
return true;
|
|
70856
|
+
}
|
|
70857
|
+
this._resyncNoOpStrikes.delete(groupKey);
|
|
70858
|
+
console.warn(
|
|
70859
|
+
`[SecureChannel] Resync for ${groupId.slice(0, 8)}: still failing at epoch=${mgr.epoch} after ${_SecureChannel.MAX_RESYNC_NOOPS} no-op resyncs \u2014 escalating to re-key`
|
|
70860
|
+
);
|
|
70321
70861
|
return false;
|
|
70322
70862
|
}
|
|
70323
70863
|
await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mgr.exportState()));
|
|
70324
70864
|
this._mlsDecryptFailCounts.delete(groupKey);
|
|
70865
|
+
this._resyncNoOpStrikes.delete(groupKey);
|
|
70325
70866
|
console.log(
|
|
70326
70867
|
`[SecureChannel] Resync-on-divergence: applied ${applied} missed commit(s) for ${groupId.slice(0, 8)}, now epoch=${mgr.epoch}`
|
|
70327
70868
|
);
|
|
@@ -70708,6 +71249,7 @@ ${messageText}`;
|
|
|
70708
71249
|
const ackedIds = [];
|
|
70709
71250
|
const nackedIds = [];
|
|
70710
71251
|
for (const msg of messages) {
|
|
71252
|
+
let handled = true;
|
|
70711
71253
|
try {
|
|
70712
71254
|
if (msg.sender_device_id === this._deviceId) {
|
|
70713
71255
|
ackedIds.push(msg.queue_id);
|
|
@@ -70724,7 +71266,7 @@ ${messageText}`;
|
|
|
70724
71266
|
a2a_channel_id: msg.a2a_channel_id
|
|
70725
71267
|
});
|
|
70726
71268
|
} else if (msg.message_type === "commit") {
|
|
70727
|
-
await this._handleMlsCommit({
|
|
71269
|
+
handled = await this._handleMlsCommit({
|
|
70728
71270
|
group_id: msg.group_id,
|
|
70729
71271
|
sender_device_id: msg.sender_device_id,
|
|
70730
71272
|
epoch: msg.epoch,
|
|
@@ -70762,9 +71304,18 @@ ${messageText}`;
|
|
|
70762
71304
|
conversation_group_id: msg.conversation_group_id,
|
|
70763
71305
|
created_at: msg.created_at
|
|
70764
71306
|
});
|
|
71307
|
+
} else {
|
|
71308
|
+
handled = false;
|
|
71309
|
+
console.warn(
|
|
71310
|
+
`[SecureChannel] Delivery application row ${String(msg.message_id).slice(0, 8)} names no room, A2A channel or conversation \u2014 NOT acked`
|
|
71311
|
+
);
|
|
70765
71312
|
}
|
|
70766
71313
|
}
|
|
70767
|
-
|
|
71314
|
+
if (handled) {
|
|
71315
|
+
ackedIds.push(msg.queue_id);
|
|
71316
|
+
} else {
|
|
71317
|
+
nackedIds.push(msg.queue_id);
|
|
71318
|
+
}
|
|
70768
71319
|
} catch (err) {
|
|
70769
71320
|
console.warn(`[SecureChannel] Delivery ${msg.message_type} processing failed:`, err);
|
|
70770
71321
|
nackedIds.push(msg.queue_id);
|
|
@@ -70833,38 +71384,20 @@ ${messageText}`;
|
|
|
70833
71384
|
if (!kpHex) continue;
|
|
70834
71385
|
const kpBytes = new Uint8Array(Buffer.from(kpHex, "hex"));
|
|
70835
71386
|
const memberKp = MLSGroupManager.deserializeKeyPackage(kpBytes);
|
|
70836
|
-
const
|
|
70837
|
-
|
|
70838
|
-
|
|
70839
|
-
|
|
70840
|
-
|
|
70841
|
-
|
|
70842
|
-
|
|
70843
|
-
|
|
70844
|
-
|
|
70845
|
-
}
|
|
70846
|
-
}
|
|
70847
|
-
if (
|
|
70848
|
-
|
|
70849
|
-
event: "mls_welcome",
|
|
70850
|
-
data: {
|
|
70851
|
-
target_device_id: req.requesting_device_id,
|
|
70852
|
-
group_id: roomState.mlsGroupId,
|
|
70853
|
-
room_id: roomId,
|
|
70854
|
-
payload: Buffer.from(welcome).toString("hex")
|
|
70855
|
-
}
|
|
70856
|
-
}));
|
|
71387
|
+
const done = await this._fulfilWelcomeRequests({
|
|
71388
|
+
mlsGroup,
|
|
71389
|
+
mlsGroupId: roomState.mlsGroupId,
|
|
71390
|
+
label: `room ${roomId.slice(0, 8)}`,
|
|
71391
|
+
welcomeFields: { room_id: roomId },
|
|
71392
|
+
targets: [{
|
|
71393
|
+
requestId: req.id,
|
|
71394
|
+
deviceId: req.requesting_device_id,
|
|
71395
|
+
keyPackage: memberKp
|
|
71396
|
+
}]
|
|
71397
|
+
});
|
|
71398
|
+
if (done > 0) {
|
|
71399
|
+
console.log(`[SecureChannel] Fulfilled Welcome for ${req.requesting_device_id.slice(0, 8)} in room ${roomId.slice(0, 8)}`);
|
|
70857
71400
|
}
|
|
70858
|
-
await fetch(
|
|
70859
|
-
`${this.config.apiUrl}/api/v1/mls/groups/${roomState.mlsGroupId}/fulfill-welcome`,
|
|
70860
|
-
{
|
|
70861
|
-
method: "POST",
|
|
70862
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
70863
|
-
body: JSON.stringify({ request_id: req.id })
|
|
70864
|
-
}
|
|
70865
|
-
);
|
|
70866
|
-
await saveMlsState(this.config.dataDir, roomState.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
70867
|
-
console.log(`[SecureChannel] Fulfilled Welcome for ${req.requesting_device_id.slice(0, 8)} in room ${roomId.slice(0, 8)}`);
|
|
70868
71401
|
} catch (fulfillErr) {
|
|
70869
71402
|
console.warn(`[SecureChannel] Welcome fulfill failed for ${req.requesting_device_id.slice(0, 8)}:`, fulfillErr);
|
|
70870
71403
|
}
|
|
@@ -70903,65 +71436,38 @@ ${messageText}`;
|
|
|
70903
71436
|
validReqs.push({ req, kp: MLSGroupManager.deserializeKeyPackage(kpBytes) });
|
|
70904
71437
|
}
|
|
70905
71438
|
if (validReqs.length === 0) continue;
|
|
71439
|
+
const sharedTargets = validReqs.map((v2) => ({
|
|
71440
|
+
requestId: v2.req.id,
|
|
71441
|
+
deviceId: v2.req.requesting_device_id,
|
|
71442
|
+
keyPackage: v2.kp
|
|
71443
|
+
}));
|
|
70906
71444
|
try {
|
|
70907
|
-
const
|
|
70908
|
-
|
|
70909
|
-
|
|
70910
|
-
|
|
70911
|
-
|
|
70912
|
-
|
|
70913
|
-
|
|
70914
|
-
|
|
70915
|
-
|
|
70916
|
-
}));
|
|
70917
|
-
}
|
|
70918
|
-
if (welcome && this._ws) {
|
|
70919
|
-
for (const { req } of validReqs) {
|
|
70920
|
-
this._ws.send(JSON.stringify({
|
|
70921
|
-
event: "mls_welcome",
|
|
70922
|
-
data: {
|
|
70923
|
-
target_device_id: req.requesting_device_id,
|
|
70924
|
-
group_id: entry.mlsGroupId,
|
|
70925
|
-
conversation_group_id: gid,
|
|
70926
|
-
payload: Buffer.from(welcome).toString("hex")
|
|
70927
|
-
}
|
|
70928
|
-
}));
|
|
70929
|
-
}
|
|
70930
|
-
}
|
|
70931
|
-
for (const { req } of validReqs) {
|
|
70932
|
-
await fetch(
|
|
70933
|
-
`${this.config.apiUrl}/api/v1/mls/groups/${entry.mlsGroupId}/fulfill-welcome`,
|
|
70934
|
-
{
|
|
70935
|
-
method: "POST",
|
|
70936
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
70937
|
-
body: JSON.stringify({ request_id: req.id })
|
|
70938
|
-
}
|
|
70939
|
-
).catch(() => {
|
|
70940
|
-
});
|
|
71445
|
+
const done = await this._fulfilWelcomeRequests({
|
|
71446
|
+
mlsGroup,
|
|
71447
|
+
mlsGroupId: entry.mlsGroupId,
|
|
71448
|
+
label: `shared group ${gid.slice(0, 8)}`,
|
|
71449
|
+
welcomeFields: { conversation_group_id: gid },
|
|
71450
|
+
targets: sharedTargets
|
|
71451
|
+
});
|
|
71452
|
+
if (done > 0) {
|
|
71453
|
+
console.log(`[SecureChannel] Batched Welcome for ${done}/${validReqs.length} devices in shared group ${gid.slice(0, 8)} (epoch=${mlsGroup.epoch})`);
|
|
70941
71454
|
}
|
|
70942
|
-
await saveMlsState(this.config.dataDir, entry.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
70943
|
-
console.log(`[SecureChannel] Batched Welcome for ${validReqs.length} devices in shared group ${gid.slice(0, 8)} (epoch=${mlsGroup.epoch})`);
|
|
70944
71455
|
} catch (batchErr) {
|
|
70945
71456
|
console.warn(`[SecureChannel] Batched Welcome failed for shared group ${gid.slice(0, 8)}, falling back to individual:`, batchErr);
|
|
70946
|
-
for (const
|
|
71457
|
+
for (const target of sharedTargets) {
|
|
70947
71458
|
try {
|
|
70948
|
-
const
|
|
70949
|
-
|
|
70950
|
-
|
|
70951
|
-
|
|
70952
|
-
|
|
70953
|
-
|
|
70954
|
-
}
|
|
70955
|
-
await fetch(`${this.config.apiUrl}/api/v1/mls/groups/${entry.mlsGroupId}/fulfill-welcome`, {
|
|
70956
|
-
method: "POST",
|
|
70957
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
70958
|
-
body: JSON.stringify({ request_id: req.id })
|
|
70959
|
-
}).catch(() => {
|
|
71459
|
+
const done = await this._fulfilWelcomeRequests({
|
|
71460
|
+
mlsGroup,
|
|
71461
|
+
mlsGroupId: entry.mlsGroupId,
|
|
71462
|
+
label: `shared group ${gid.slice(0, 8)}`,
|
|
71463
|
+
welcomeFields: { conversation_group_id: gid },
|
|
71464
|
+
targets: [target]
|
|
70960
71465
|
});
|
|
70961
|
-
|
|
70962
|
-
|
|
71466
|
+
if (done > 0) {
|
|
71467
|
+
console.log(`[SecureChannel] Individual Welcome for ${target.deviceId.slice(0, 8)} in shared group ${gid.slice(0, 8)}`);
|
|
71468
|
+
}
|
|
70963
71469
|
} catch (indivErr) {
|
|
70964
|
-
console.warn(`[SecureChannel] Individual Welcome failed for ${
|
|
71470
|
+
console.warn(`[SecureChannel] Individual Welcome failed for ${target.deviceId.slice(0, 8)}:`, indivErr);
|
|
70965
71471
|
}
|
|
70966
71472
|
}
|
|
70967
71473
|
}
|
|
@@ -70993,38 +71499,20 @@ ${messageText}`;
|
|
|
70993
71499
|
if (!kpHex) continue;
|
|
70994
71500
|
const kpBytes = new Uint8Array(Buffer.from(kpHex, "hex"));
|
|
70995
71501
|
const memberKp = MLSGroupManager.deserializeKeyPackage(kpBytes);
|
|
70996
|
-
const
|
|
70997
|
-
|
|
70998
|
-
|
|
70999
|
-
|
|
71000
|
-
|
|
71001
|
-
|
|
71002
|
-
|
|
71003
|
-
|
|
71004
|
-
|
|
71005
|
-
}
|
|
71006
|
-
}
|
|
71007
|
-
if (
|
|
71008
|
-
|
|
71009
|
-
event: "mls_welcome",
|
|
71010
|
-
data: {
|
|
71011
|
-
target_device_id: req.requesting_device_id,
|
|
71012
|
-
group_id: convEntry.mlsGroupId,
|
|
71013
|
-
conversation_id: convId,
|
|
71014
|
-
payload: Buffer.from(welcome).toString("hex")
|
|
71015
|
-
}
|
|
71016
|
-
}));
|
|
71502
|
+
const done = await this._fulfilWelcomeRequests({
|
|
71503
|
+
mlsGroup,
|
|
71504
|
+
mlsGroupId: convEntry.mlsGroupId,
|
|
71505
|
+
label: `conv ${convId.slice(0, 8)}`,
|
|
71506
|
+
welcomeFields: { conversation_id: convId },
|
|
71507
|
+
targets: [{
|
|
71508
|
+
requestId: req.id,
|
|
71509
|
+
deviceId: req.requesting_device_id,
|
|
71510
|
+
keyPackage: memberKp
|
|
71511
|
+
}]
|
|
71512
|
+
});
|
|
71513
|
+
if (done > 0) {
|
|
71514
|
+
console.log(`[SecureChannel] Fulfilled Welcome for ${req.requesting_device_id.slice(0, 8)} in conv ${convId.slice(0, 8)}`);
|
|
71017
71515
|
}
|
|
71018
|
-
await fetch(
|
|
71019
|
-
`${this.config.apiUrl}/api/v1/mls/groups/${convEntry.mlsGroupId}/fulfill-welcome`,
|
|
71020
|
-
{
|
|
71021
|
-
method: "POST",
|
|
71022
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
71023
|
-
body: JSON.stringify({ request_id: req.id })
|
|
71024
|
-
}
|
|
71025
|
-
);
|
|
71026
|
-
await saveMlsState(this.config.dataDir, convEntry.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
71027
|
-
console.log(`[SecureChannel] Fulfilled Welcome for ${req.requesting_device_id.slice(0, 8)} in conv ${convId.slice(0, 8)}`);
|
|
71028
71516
|
} catch (fulfillErr) {
|
|
71029
71517
|
console.warn(`[SecureChannel] Conv Welcome fulfill failed for ${req.requesting_device_id.slice(0, 8)}:`, fulfillErr);
|
|
71030
71518
|
}
|
|
@@ -71205,12 +71693,13 @@ ${messageText}`;
|
|
|
71205
71693
|
} catch (distErr) {
|
|
71206
71694
|
console.warn(`[SecureChannel] A2A distribute network error:`, distErr);
|
|
71207
71695
|
}
|
|
71208
|
-
if (!distributed && this.
|
|
71209
|
-
|
|
71696
|
+
if (!distributed && this._wsIsOpen()) {
|
|
71697
|
+
const a2aLabel = `A2A ${chId.slice(0, 8)}`;
|
|
71698
|
+
const commitSent = await this._sendFrameConfirmed({
|
|
71210
71699
|
event: "mls_commit",
|
|
71211
71700
|
data: { group_id: chState.mlsGroupId, epoch: Number(mlsGroup.epoch), payload: commitHex }
|
|
71212
|
-
})
|
|
71213
|
-
this.
|
|
71701
|
+
}, `${a2aLabel} commit`);
|
|
71702
|
+
const welcomeSent = commitSent && await this._sendFrameConfirmed({
|
|
71214
71703
|
event: "mls_welcome",
|
|
71215
71704
|
data: {
|
|
71216
71705
|
target_device_id: req.requesting_device_id,
|
|
@@ -71218,7 +71707,11 @@ ${messageText}`;
|
|
|
71218
71707
|
a2a_channel_id: chId,
|
|
71219
71708
|
payload: welcomeHex
|
|
71220
71709
|
}
|
|
71221
|
-
})
|
|
71710
|
+
}, `${a2aLabel} welcome`);
|
|
71711
|
+
if (!welcomeSent) {
|
|
71712
|
+
console.warn(`[SecureChannel] ${a2aLabel}: Welcome frames were NOT written \u2014 leaving the request pending`);
|
|
71713
|
+
continue;
|
|
71714
|
+
}
|
|
71222
71715
|
try {
|
|
71223
71716
|
await fetch(
|
|
71224
71717
|
`${this.config.apiUrl}/api/v1/mls/groups/${chState.mlsGroupId}/distribute`,
|
|
@@ -71241,16 +71734,15 @@ ${messageText}`;
|
|
|
71241
71734
|
console.warn(`[SecureChannel] Cannot deliver Welcome \u2014 HTTP distribute and WS both unavailable`);
|
|
71242
71735
|
continue;
|
|
71243
71736
|
}
|
|
71244
|
-
await
|
|
71245
|
-
|
|
71246
|
-
|
|
71247
|
-
|
|
71248
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
71249
|
-
body: JSON.stringify({ request_id: req.id })
|
|
71250
|
-
}
|
|
71737
|
+
const a2aFulfilled = await this._postFulfilWelcome(
|
|
71738
|
+
chState.mlsGroupId,
|
|
71739
|
+
req.id,
|
|
71740
|
+
`A2A ${chId.slice(0, 8)}`
|
|
71251
71741
|
);
|
|
71252
71742
|
await saveMlsState(this.config.dataDir, chState.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
71253
|
-
|
|
71743
|
+
if (a2aFulfilled) {
|
|
71744
|
+
console.log(`[SecureChannel] Fulfilled A2A Welcome for ${req.requesting_device_id.slice(0, 8)} in channel ${chId.slice(0, 8)}`);
|
|
71745
|
+
}
|
|
71254
71746
|
} catch (fulfillErr) {
|
|
71255
71747
|
console.log(`[SecureChannel] A2A Welcome fulfill failed: ${fulfillErr instanceof Error ? fulfillErr.message : String(fulfillErr)}`);
|
|
71256
71748
|
}
|
|
@@ -71424,12 +71916,12 @@ ${messageText}`;
|
|
|
71424
71916
|
console.log(`[SecureChannel] MLS sync: re-join needed for group ${groupId?.slice(0, 8)}`);
|
|
71425
71917
|
if (groupId) {
|
|
71426
71918
|
await deleteMlsState(this.config.dataDir, groupId);
|
|
71427
|
-
|
|
71428
|
-
|
|
71429
|
-
|
|
71430
|
-
|
|
71431
|
-
}
|
|
71919
|
+
const target = this._resolveGroupFamily(groupId);
|
|
71920
|
+
if (target) {
|
|
71921
|
+
this._mlsGroups.delete(target.managerKey);
|
|
71922
|
+
console.log(`[SecureChannel] MLS sync rejoin: dropped local state for ${target.label}`);
|
|
71432
71923
|
}
|
|
71924
|
+
void this._requestWelcomeSelfHeal(groupId);
|
|
71433
71925
|
}
|
|
71434
71926
|
} else if (action === "replay_complete") {
|
|
71435
71927
|
console.log(`[SecureChannel] MLS sync complete for group ${groupId?.slice(0, 8)} (${data.count} messages replayed)`);
|
|
@@ -71441,6 +71933,7 @@ ${messageText}`;
|
|
|
71441
71933
|
*/
|
|
71442
71934
|
async _handleA2AMessageMLS(data) {
|
|
71443
71935
|
const groupId = data.group_id;
|
|
71936
|
+
this._noteGroupEpoch(groupId, data.epoch);
|
|
71444
71937
|
const channelId = data.channel_id ?? data.a2a_channel_id;
|
|
71445
71938
|
let a2aChannelId = channelId;
|
|
71446
71939
|
if (!a2aChannelId) {
|
|
@@ -72573,6 +73066,45 @@ var init_gateway_send = __esm2({
|
|
|
72573
73066
|
init_openclaw_compat();
|
|
72574
73067
|
}
|
|
72575
73068
|
});
|
|
73069
|
+
function parseMentions(text) {
|
|
73070
|
+
const mentions = [];
|
|
73071
|
+
const re2 = /@(\w[\w]*)/gi;
|
|
73072
|
+
let match;
|
|
73073
|
+
while ((match = re2.exec(text)) !== null) {
|
|
73074
|
+
mentions.push(match[1].toLowerCase());
|
|
73075
|
+
}
|
|
73076
|
+
return mentions;
|
|
73077
|
+
}
|
|
73078
|
+
function owesRoomReply(plaintext, agentName, accountId, senderIsAgent, roomDisplayName, ownDisplayName, selfResolved = true) {
|
|
73079
|
+
const mentions = parseMentions(plaintext);
|
|
73080
|
+
if (mentions.length === 0) return false;
|
|
73081
|
+
if (mentions.includes("all") || mentions.includes("everyone")) {
|
|
73082
|
+
return senderIsAgent !== true;
|
|
73083
|
+
}
|
|
73084
|
+
const ownNames = ownRoomNames(agentName, accountId, roomDisplayName, ownDisplayName);
|
|
73085
|
+
if (mentions.some((m2) => ownNames.has(m2))) return true;
|
|
73086
|
+
if (!selfResolved) return true;
|
|
73087
|
+
return false;
|
|
73088
|
+
}
|
|
73089
|
+
function ownRoomNames(agentName, accountId, roomDisplayName, ownDisplayName) {
|
|
73090
|
+
const names = /* @__PURE__ */ new Set();
|
|
73091
|
+
const add4 = (n2) => {
|
|
73092
|
+
if (!n2) return;
|
|
73093
|
+
const lower = n2.toLowerCase();
|
|
73094
|
+
names.add(lower);
|
|
73095
|
+
names.add(lower.split(/[\s(]/)[0]);
|
|
73096
|
+
};
|
|
73097
|
+
add4(agentName);
|
|
73098
|
+
add4(accountId);
|
|
73099
|
+
add4(roomDisplayName);
|
|
73100
|
+
add4(ownDisplayName);
|
|
73101
|
+
return names;
|
|
73102
|
+
}
|
|
73103
|
+
var init_room_protocol = __esm2({
|
|
73104
|
+
"src/room-protocol.ts"() {
|
|
73105
|
+
"use strict";
|
|
73106
|
+
}
|
|
73107
|
+
});
|
|
72576
73108
|
var init_llm_response_parser = __esm2({
|
|
72577
73109
|
"src/llm-response-parser.ts"() {
|
|
72578
73110
|
"use strict";
|
|
@@ -72606,6 +73138,7 @@ var isUsingManagedRoutes;
|
|
|
72606
73138
|
var init_openclaw_entry = __esm2({
|
|
72607
73139
|
"src/openclaw-entry.ts"() {
|
|
72608
73140
|
"use strict";
|
|
73141
|
+
init_room_protocol();
|
|
72609
73142
|
init_account_config();
|
|
72610
73143
|
init_fetch_interceptor();
|
|
72611
73144
|
init_http_handlers();
|
|
@@ -98664,7 +99197,8 @@ var init_index = __esm2({
|
|
|
98664
99197
|
init_skill_invoker();
|
|
98665
99198
|
await init_skill_telemetry();
|
|
98666
99199
|
await init_policy_enforcer();
|
|
98667
|
-
|
|
99200
|
+
init_room_protocol();
|
|
99201
|
+
VERSION = true ? "0.23.29" : "0.0.0-dev";
|
|
98668
99202
|
}
|
|
98669
99203
|
});
|
|
98670
99204
|
await init_index();
|
|
@@ -99417,9 +99951,32 @@ function roomTurnPrefix(roomId, senderName, roomName) {
|
|
|
99417
99951
|
const room = roomName ? `"${roomName}"` : roomId.slice(0, 8);
|
|
99418
99952
|
return `[room ${room} | ${senderName}]`;
|
|
99419
99953
|
}
|
|
99954
|
+
function _makeOwesReply(agentName, channel, log) {
|
|
99955
|
+
let warnedNoIdentity = false;
|
|
99956
|
+
return (e) => {
|
|
99957
|
+
const own = agentName ?? "";
|
|
99958
|
+
const hub = channel.ownDisplayName ?? "";
|
|
99959
|
+
if (!own && !hub && !warnedNoIdentity) {
|
|
99960
|
+
warnedNoIdentity = true;
|
|
99961
|
+
log(
|
|
99962
|
+
"WARNING: this bridge has no agentName and no hub display_name, so it cannot recognise its own @mention (#1107). It will answer @all and any @-addressed message (fail-open), but targeted replies will be unreliable. Set the agent name in the bridge config."
|
|
99963
|
+
);
|
|
99964
|
+
}
|
|
99965
|
+
return owesRoomReply(
|
|
99966
|
+
e.plaintext,
|
|
99967
|
+
own,
|
|
99968
|
+
own,
|
|
99969
|
+
e.senderIsAgent,
|
|
99970
|
+
void 0,
|
|
99971
|
+
hub,
|
|
99972
|
+
Boolean(own || hub)
|
|
99973
|
+
);
|
|
99974
|
+
};
|
|
99975
|
+
}
|
|
99420
99976
|
function wireBridge(channel, session, target, opts = {}) {
|
|
99421
99977
|
const log = opts.log ?? (() => {
|
|
99422
99978
|
});
|
|
99979
|
+
const _owesReply = _makeOwesReply(opts.agentName, channel, log);
|
|
99423
99980
|
channel.on("error", (err) => {
|
|
99424
99981
|
const msg = err instanceof Error ? err.message : String(err);
|
|
99425
99982
|
log(`channel error (non-fatal, not a reconnect trigger): ${msg}`);
|
|
@@ -99458,7 +100015,7 @@ function wireBridge(channel, session, target, opts = {}) {
|
|
|
99458
100015
|
target.setRoom(e.roomId);
|
|
99459
100016
|
session.push(`${roomTurnPrefix(e.roomId, e.senderName, roomNames.get(e.roomId))}: ${e.plaintext}`, target.snapshotReply(channel, log), {
|
|
99460
100017
|
autoReplyOnText: false,
|
|
99461
|
-
replyExpected: e
|
|
100018
|
+
replyExpected: _owesReply(e)
|
|
99462
100019
|
});
|
|
99463
100020
|
});
|
|
99464
100021
|
channel.on("message", (text, metadata) => {
|
|
@@ -99567,7 +100124,7 @@ async function main() {
|
|
|
99567
100124
|
const info = parseInfoFlags(process.argv.slice(2));
|
|
99568
100125
|
if (info.showVersion || info.showHelp) {
|
|
99569
100126
|
const version2 = buildClientVersion2(
|
|
99570
|
-
true ? "0.4.
|
|
100127
|
+
true ? "0.4.2" : void 0
|
|
99571
100128
|
);
|
|
99572
100129
|
if (info.showVersion) printLine(version2);
|
|
99573
100130
|
if (info.showHelp) {
|
|
@@ -99620,7 +100177,7 @@ async function main() {
|
|
|
99620
100177
|
agentName: cfg.agentName,
|
|
99621
100178
|
platform: "node",
|
|
99622
100179
|
clientVersion: buildClientVersion2(
|
|
99623
|
-
true ? "0.4.
|
|
100180
|
+
true ? "0.4.2" : void 0
|
|
99624
100181
|
)
|
|
99625
100182
|
});
|
|
99626
100183
|
channelRef = channel;
|
|
@@ -99700,6 +100257,9 @@ async function main() {
|
|
|
99700
100257
|
const target = new ActiveTarget();
|
|
99701
100258
|
wireBridge(channel, session, target, {
|
|
99702
100259
|
roomFilter: cfg.roomFilter,
|
|
100260
|
+
// #1107: needed to recognise this agent's own @mention. Without it the
|
|
100261
|
+
// agent can only answer @all.
|
|
100262
|
+
agentName: cfg.agentName,
|
|
99703
100263
|
log: (m2) => logLine("[grok-bridge] " + m2)
|
|
99704
100264
|
});
|
|
99705
100265
|
attachLifecycle2(channel, {
|