@voidly/agent-sdk 3.2.7 → 3.3.1

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/index.d.ts CHANGED
@@ -96,6 +96,10 @@ interface VoidlyAgentConfig {
96
96
  /** Transport preference for listen() — tries in order, falls back automatically
97
97
  * Default: ['sse', 'long-poll']. Options: 'websocket' | 'sse' | 'long-poll' */
98
98
  transport?: ('websocket' | 'sse' | 'long-poll')[];
99
+ /** Auto-reset ratchet after N consecutive decrypt failures from same peer (default: 10, 0 = disabled) */
100
+ autoResetThreshold?: number;
101
+ /** Callback when a ratchet is auto-reset due to consecutive failures */
102
+ onRatchetReset?: (peerDid: string, failCount: number) => void;
99
103
  }
100
104
  interface ListenOptions {
101
105
  /** Milliseconds between polls (default: 2000, min: 500) */
@@ -192,6 +196,10 @@ declare class VoidlyAgent {
192
196
  private _identityCache;
193
197
  private _seenMessageIds;
194
198
  private _decryptFailCount;
199
+ /** Per-peer consecutive decrypt failure tracking for auto-recovery */
200
+ private _peerDecryptFails;
201
+ /** Max consecutive per-peer decrypt failures before auto-resetting ratchet (0 = disabled) */
202
+ private _autoResetThreshold;
195
203
  private _rpcHandlers;
196
204
  private _rpcPending;
197
205
  private _coverTrafficTimer;
@@ -202,6 +210,7 @@ declare class VoidlyAgent {
202
210
  private _persistPath?;
203
211
  private _persistKey;
204
212
  private _transportPrefs;
213
+ private _onRatchetReset?;
205
214
  private constructor();
206
215
  /**
207
216
  * Register a new agent on the Voidly relay.
@@ -290,6 +299,24 @@ declare class VoidlyAgent {
290
299
  * Useful for detecting key mismatches, attacks, or corruption.
291
300
  */
292
301
  get decryptFailCount(): number;
302
+ /**
303
+ * Get per-peer consecutive decrypt failure counts.
304
+ * Useful for diagnosing which peer conversations have desynchronized ratchets.
305
+ */
306
+ get peerDecryptFails(): Record<string, number>;
307
+ /**
308
+ * Reset ratchet state for a specific peer.
309
+ * This clears all ratchet keys for the conversation, causing the next send
310
+ * to re-initialize a fresh DH exchange. The peer's ratchet will also reset
311
+ * automatically when they receive the new-format message.
312
+ *
313
+ * Use this when ratchet desync is detected (e.g., consecutive decrypt failures).
314
+ * After calling this, both sides need to send a message to re-establish the ratchet.
315
+ *
316
+ * @param peerDid - The DID of the peer whose ratchet should be reset
317
+ * @returns true if a ratchet was found and reset, false if no ratchet existed
318
+ */
319
+ resetRatchet(peerDid: string): boolean;
293
320
  /**
294
321
  * Generate a did:key identifier from this agent's Ed25519 signing key.
295
322
  * did:key is a W3C standard — interoperable across systems.
@@ -337,7 +364,11 @@ declare class VoidlyAgent {
337
364
  messageType?: string;
338
365
  unreadOnly?: boolean;
339
366
  }): Promise<DecryptedMessage[]>;
340
- /** Decrypt raw message objects (shared by receive(), SSE, WebSocket transports) */
367
+ /** Decrypt raw message objects (shared by receive(), SSE, WebSocket transports)
368
+ * Returns decrypted messages AND IDs of messages that failed to decrypt.
369
+ * Failed messages should be marked as read on the relay — they are permanently
370
+ * undecryptable and will poison the queue if left unread (Signal-style handling).
371
+ */
341
372
  private _decryptMessages;
342
373
  /**
343
374
  * Delete a message by ID (must be sender or recipient).
package/dist/index.js CHANGED
@@ -4082,6 +4082,10 @@ var VoidlyAgent = class _VoidlyAgent {
4082
4082
  this._identityCache = /* @__PURE__ */ new Map();
4083
4083
  this._seenMessageIds = /* @__PURE__ */ new Set();
4084
4084
  this._decryptFailCount = 0;
4085
+ /** Per-peer consecutive decrypt failure tracking for auto-recovery */
4086
+ this._peerDecryptFails = /* @__PURE__ */ new Map();
4087
+ /** Max consecutive per-peer decrypt failures before auto-resetting ratchet (0 = disabled) */
4088
+ this._autoResetThreshold = 10;
4085
4089
  // RPC handlers: method → handler function
4086
4090
  this._rpcHandlers = /* @__PURE__ */ new Map();
4087
4091
  // RPC pending responses: rpc_id → { resolve, reject, timer }
@@ -4117,6 +4121,8 @@ var VoidlyAgent = class _VoidlyAgent {
4117
4121
  this._onLoad = config?.onLoad;
4118
4122
  this._persistPath = config?.persistPath;
4119
4123
  this._transportPrefs = config?.transport || ["sse", "long-poll"];
4124
+ this._autoResetThreshold = config?.autoResetThreshold ?? 10;
4125
+ this._onRatchetReset = config?.onRatchetReset;
4120
4126
  }
4121
4127
  // ─── Factory Methods ────────────────────────────────────────────────────────
4122
4128
  /**
@@ -4214,6 +4220,7 @@ var VoidlyAgent = class _VoidlyAgent {
4214
4220
  } catch {
4215
4221
  }
4216
4222
  }
4223
+ const effectiveConfig = !mlkemSk && config?.postQuantum !== false ? { ...config, postQuantum: false } : config;
4217
4224
  const agent = new _VoidlyAgent({
4218
4225
  did: creds.did,
4219
4226
  apiKey: creds.apiKey,
@@ -4224,7 +4231,7 @@ var VoidlyAgent = class _VoidlyAgent {
4224
4231
  },
4225
4232
  mlkemPublicKey: mlkemPk,
4226
4233
  mlkemSecretKey: mlkemSk
4227
- }, config);
4234
+ }, effectiveConfig);
4228
4235
  if (creds.ratchetStates) {
4229
4236
  for (const [pairId, rs] of Object.entries(creds.ratchetStates)) {
4230
4237
  try {
@@ -4519,6 +4526,37 @@ var VoidlyAgent = class _VoidlyAgent {
4519
4526
  get decryptFailCount() {
4520
4527
  return this._decryptFailCount;
4521
4528
  }
4529
+ /**
4530
+ * Get per-peer consecutive decrypt failure counts.
4531
+ * Useful for diagnosing which peer conversations have desynchronized ratchets.
4532
+ */
4533
+ get peerDecryptFails() {
4534
+ return Object.fromEntries(this._peerDecryptFails);
4535
+ }
4536
+ /**
4537
+ * Reset ratchet state for a specific peer.
4538
+ * This clears all ratchet keys for the conversation, causing the next send
4539
+ * to re-initialize a fresh DH exchange. The peer's ratchet will also reset
4540
+ * automatically when they receive the new-format message.
4541
+ *
4542
+ * Use this when ratchet desync is detected (e.g., consecutive decrypt failures).
4543
+ * After calling this, both sides need to send a message to re-establish the ratchet.
4544
+ *
4545
+ * @param peerDid - The DID of the peer whose ratchet should be reset
4546
+ * @returns true if a ratchet was found and reset, false if no ratchet existed
4547
+ */
4548
+ resetRatchet(peerDid) {
4549
+ const sendPairId = `${this.did}:${peerDid}`;
4550
+ const recvPairId = `${peerDid}:${this.did}`;
4551
+ const hadSend = this._ratchetStates.delete(sendPairId);
4552
+ const hadRecv = this._ratchetStates.delete(recvPairId);
4553
+ this._peerDecryptFails.delete(peerDid);
4554
+ if (hadSend || hadRecv) {
4555
+ this._persistRatchetState().catch(() => {
4556
+ });
4557
+ }
4558
+ return hadSend || hadRecv;
4559
+ }
4522
4560
  /**
4523
4561
  * Generate a did:key identifier from this agent's Ed25519 signing key.
4524
4562
  * did:key is a W3C standard — interoperable across systems.
@@ -4747,14 +4785,37 @@ var VoidlyAgent = class _VoidlyAgent {
4747
4785
  throw new Error(`Receive failed: ${err.error?.message || err.error || res.statusText}`);
4748
4786
  }
4749
4787
  const data = await res.json();
4750
- return this._decryptMessages(data.messages);
4788
+ const { decrypted, failedIds } = await this._decryptMessages(data.messages);
4789
+ if (failedIds.length > 0) {
4790
+ try {
4791
+ await this.markReadBatch(failedIds);
4792
+ } catch {
4793
+ for (const id of failedIds) {
4794
+ try {
4795
+ await this.markRead(id);
4796
+ } catch {
4797
+ }
4798
+ }
4799
+ }
4800
+ }
4801
+ return decrypted;
4751
4802
  }
4752
- /** Decrypt raw message objects (shared by receive(), SSE, WebSocket transports) */
4803
+ /** Decrypt raw message objects (shared by receive(), SSE, WebSocket transports)
4804
+ * Returns decrypted messages AND IDs of messages that failed to decrypt.
4805
+ * Failed messages should be marked as read on the relay — they are permanently
4806
+ * undecryptable and will poison the queue if left unread (Signal-style handling).
4807
+ */
4753
4808
  async _decryptMessages(rawMessages) {
4754
4809
  const decrypted = [];
4810
+ const failedIds = [];
4811
+ const resetPeers = /* @__PURE__ */ new Set();
4755
4812
  for (const msg of rawMessages) {
4756
4813
  try {
4757
4814
  if (this._seenMessageIds.has(msg.id)) continue;
4815
+ if (resetPeers.has(msg.from)) {
4816
+ failedIds.push(msg.id);
4817
+ continue;
4818
+ }
4758
4819
  let senderEncPub;
4759
4820
  let senderSignPubBytes = null;
4760
4821
  if (msg.sender_encryption_key) {
@@ -4890,6 +4951,15 @@ var VoidlyAgent = class _VoidlyAgent {
4890
4951
  const skip = targetStep - state.recvStep;
4891
4952
  if (skip > MAX_SKIP) {
4892
4953
  this._decryptFailCount++;
4954
+ failedIds.push(msg.id);
4955
+ const peerForSkip = msg.from || "unknown";
4956
+ const prevSkipFails = this._peerDecryptFails.get(peerForSkip) || 0;
4957
+ this._peerDecryptFails.set(peerForSkip, prevSkipFails + 1);
4958
+ if (this._autoResetThreshold > 0 && prevSkipFails + 1 >= this._autoResetThreshold) {
4959
+ this.resetRatchet(peerForSkip);
4960
+ resetPeers.add(peerForSkip);
4961
+ this._onRatchetReset?.(peerForSkip, prevSkipFails + 1);
4962
+ }
4893
4963
  continue;
4894
4964
  } else {
4895
4965
  let ck = state.recvChainKey;
@@ -4917,8 +4987,23 @@ var VoidlyAgent = class _VoidlyAgent {
4917
4987
  }
4918
4988
  if (!rawPlaintext) {
4919
4989
  this._decryptFailCount++;
4990
+ failedIds.push(msg.id);
4991
+ const senderForFail = msg.from || "unknown";
4992
+ const prevFails = this._peerDecryptFails.get(senderForFail) || 0;
4993
+ this._peerDecryptFails.set(senderForFail, prevFails + 1);
4994
+ if (this._autoResetThreshold > 0 && prevFails + 1 >= this._autoResetThreshold) {
4995
+ this.resetRatchet(senderForFail);
4996
+ resetPeers.add(senderForFail);
4997
+ this._onRatchetReset?.(senderForFail, prevFails + 1);
4998
+ }
4920
4999
  continue;
4921
5000
  }
5001
+ {
5002
+ const senderForSuccess = msg.from || "unknown";
5003
+ if (this._peerDecryptFails.has(senderForSuccess)) {
5004
+ this._peerDecryptFails.delete(senderForSuccess);
5005
+ }
5006
+ }
4922
5007
  let plaintextBytes = rawPlaintext;
4923
5008
  let wasPadded = false;
4924
5009
  let wasSealed = false;
@@ -4983,6 +5068,10 @@ var VoidlyAgent = class _VoidlyAgent {
4983
5068
  }
4984
5069
  if (this.requireSignatures && !signatureValid) {
4985
5070
  this._decryptFailCount++;
5071
+ failedIds.push(msg.id);
5072
+ const peerForSig = msg.from || "unknown";
5073
+ const prevSigFails = this._peerDecryptFails.get(peerForSig) || 0;
5074
+ this._peerDecryptFails.set(peerForSig, prevSigFails + 1);
4986
5075
  continue;
4987
5076
  }
4988
5077
  this._seenMessageIds.add(msg.id);
@@ -5009,13 +5098,22 @@ var VoidlyAgent = class _VoidlyAgent {
5009
5098
  });
5010
5099
  } catch {
5011
5100
  this._decryptFailCount++;
5101
+ failedIds.push(msg.id);
5102
+ const peerForCatch = msg.from || "unknown";
5103
+ const prevCatchFails = this._peerDecryptFails.get(peerForCatch) || 0;
5104
+ this._peerDecryptFails.set(peerForCatch, prevCatchFails + 1);
5105
+ if (this._autoResetThreshold > 0 && prevCatchFails + 1 >= this._autoResetThreshold) {
5106
+ this.resetRatchet(peerForCatch);
5107
+ resetPeers.add(peerForCatch);
5108
+ this._onRatchetReset?.(peerForCatch, prevCatchFails + 1);
5109
+ }
5012
5110
  }
5013
5111
  }
5014
- if (decrypted.length > 0) {
5112
+ if (decrypted.length > 0 || this._peerDecryptFails.size > 0) {
5015
5113
  this._persistRatchetState().catch(() => {
5016
5114
  });
5017
5115
  }
5018
- return decrypted;
5116
+ return { decrypted, failedIds };
5019
5117
  }
5020
5118
  // ─── Message Management ─────────────────────────────────────────────────────
5021
5119
  /**
@@ -6371,7 +6469,13 @@ var VoidlyAgent = class _VoidlyAgent {
6371
6469
  if (eventType === "message" && dataStr) {
6372
6470
  try {
6373
6471
  const rawMsg = JSON.parse(dataStr);
6374
- const decrypted = await this._decryptMessages([rawMsg]);
6472
+ const { decrypted, failedIds } = await this._decryptMessages([rawMsg]);
6473
+ for (const id of failedIds) {
6474
+ try {
6475
+ await this.markRead(id);
6476
+ } catch {
6477
+ }
6478
+ }
6375
6479
  if (decrypted.length > 0) {
6376
6480
  consecutiveEmpty = 0;
6377
6481
  sseFailures = 0;
package/dist/index.mjs CHANGED
@@ -4072,6 +4072,10 @@ var VoidlyAgent = class _VoidlyAgent {
4072
4072
  this._identityCache = /* @__PURE__ */ new Map();
4073
4073
  this._seenMessageIds = /* @__PURE__ */ new Set();
4074
4074
  this._decryptFailCount = 0;
4075
+ /** Per-peer consecutive decrypt failure tracking for auto-recovery */
4076
+ this._peerDecryptFails = /* @__PURE__ */ new Map();
4077
+ /** Max consecutive per-peer decrypt failures before auto-resetting ratchet (0 = disabled) */
4078
+ this._autoResetThreshold = 10;
4075
4079
  // RPC handlers: method → handler function
4076
4080
  this._rpcHandlers = /* @__PURE__ */ new Map();
4077
4081
  // RPC pending responses: rpc_id → { resolve, reject, timer }
@@ -4107,6 +4111,8 @@ var VoidlyAgent = class _VoidlyAgent {
4107
4111
  this._onLoad = config?.onLoad;
4108
4112
  this._persistPath = config?.persistPath;
4109
4113
  this._transportPrefs = config?.transport || ["sse", "long-poll"];
4114
+ this._autoResetThreshold = config?.autoResetThreshold ?? 10;
4115
+ this._onRatchetReset = config?.onRatchetReset;
4110
4116
  }
4111
4117
  // ─── Factory Methods ────────────────────────────────────────────────────────
4112
4118
  /**
@@ -4204,6 +4210,7 @@ var VoidlyAgent = class _VoidlyAgent {
4204
4210
  } catch {
4205
4211
  }
4206
4212
  }
4213
+ const effectiveConfig = !mlkemSk && config?.postQuantum !== false ? { ...config, postQuantum: false } : config;
4207
4214
  const agent = new _VoidlyAgent({
4208
4215
  did: creds.did,
4209
4216
  apiKey: creds.apiKey,
@@ -4214,7 +4221,7 @@ var VoidlyAgent = class _VoidlyAgent {
4214
4221
  },
4215
4222
  mlkemPublicKey: mlkemPk,
4216
4223
  mlkemSecretKey: mlkemSk
4217
- }, config);
4224
+ }, effectiveConfig);
4218
4225
  if (creds.ratchetStates) {
4219
4226
  for (const [pairId, rs] of Object.entries(creds.ratchetStates)) {
4220
4227
  try {
@@ -4509,6 +4516,37 @@ var VoidlyAgent = class _VoidlyAgent {
4509
4516
  get decryptFailCount() {
4510
4517
  return this._decryptFailCount;
4511
4518
  }
4519
+ /**
4520
+ * Get per-peer consecutive decrypt failure counts.
4521
+ * Useful for diagnosing which peer conversations have desynchronized ratchets.
4522
+ */
4523
+ get peerDecryptFails() {
4524
+ return Object.fromEntries(this._peerDecryptFails);
4525
+ }
4526
+ /**
4527
+ * Reset ratchet state for a specific peer.
4528
+ * This clears all ratchet keys for the conversation, causing the next send
4529
+ * to re-initialize a fresh DH exchange. The peer's ratchet will also reset
4530
+ * automatically when they receive the new-format message.
4531
+ *
4532
+ * Use this when ratchet desync is detected (e.g., consecutive decrypt failures).
4533
+ * After calling this, both sides need to send a message to re-establish the ratchet.
4534
+ *
4535
+ * @param peerDid - The DID of the peer whose ratchet should be reset
4536
+ * @returns true if a ratchet was found and reset, false if no ratchet existed
4537
+ */
4538
+ resetRatchet(peerDid) {
4539
+ const sendPairId = `${this.did}:${peerDid}`;
4540
+ const recvPairId = `${peerDid}:${this.did}`;
4541
+ const hadSend = this._ratchetStates.delete(sendPairId);
4542
+ const hadRecv = this._ratchetStates.delete(recvPairId);
4543
+ this._peerDecryptFails.delete(peerDid);
4544
+ if (hadSend || hadRecv) {
4545
+ this._persistRatchetState().catch(() => {
4546
+ });
4547
+ }
4548
+ return hadSend || hadRecv;
4549
+ }
4512
4550
  /**
4513
4551
  * Generate a did:key identifier from this agent's Ed25519 signing key.
4514
4552
  * did:key is a W3C standard — interoperable across systems.
@@ -4737,14 +4775,37 @@ var VoidlyAgent = class _VoidlyAgent {
4737
4775
  throw new Error(`Receive failed: ${err.error?.message || err.error || res.statusText}`);
4738
4776
  }
4739
4777
  const data = await res.json();
4740
- return this._decryptMessages(data.messages);
4778
+ const { decrypted, failedIds } = await this._decryptMessages(data.messages);
4779
+ if (failedIds.length > 0) {
4780
+ try {
4781
+ await this.markReadBatch(failedIds);
4782
+ } catch {
4783
+ for (const id of failedIds) {
4784
+ try {
4785
+ await this.markRead(id);
4786
+ } catch {
4787
+ }
4788
+ }
4789
+ }
4790
+ }
4791
+ return decrypted;
4741
4792
  }
4742
- /** Decrypt raw message objects (shared by receive(), SSE, WebSocket transports) */
4793
+ /** Decrypt raw message objects (shared by receive(), SSE, WebSocket transports)
4794
+ * Returns decrypted messages AND IDs of messages that failed to decrypt.
4795
+ * Failed messages should be marked as read on the relay — they are permanently
4796
+ * undecryptable and will poison the queue if left unread (Signal-style handling).
4797
+ */
4743
4798
  async _decryptMessages(rawMessages) {
4744
4799
  const decrypted = [];
4800
+ const failedIds = [];
4801
+ const resetPeers = /* @__PURE__ */ new Set();
4745
4802
  for (const msg of rawMessages) {
4746
4803
  try {
4747
4804
  if (this._seenMessageIds.has(msg.id)) continue;
4805
+ if (resetPeers.has(msg.from)) {
4806
+ failedIds.push(msg.id);
4807
+ continue;
4808
+ }
4748
4809
  let senderEncPub;
4749
4810
  let senderSignPubBytes = null;
4750
4811
  if (msg.sender_encryption_key) {
@@ -4880,6 +4941,15 @@ var VoidlyAgent = class _VoidlyAgent {
4880
4941
  const skip = targetStep - state.recvStep;
4881
4942
  if (skip > MAX_SKIP) {
4882
4943
  this._decryptFailCount++;
4944
+ failedIds.push(msg.id);
4945
+ const peerForSkip = msg.from || "unknown";
4946
+ const prevSkipFails = this._peerDecryptFails.get(peerForSkip) || 0;
4947
+ this._peerDecryptFails.set(peerForSkip, prevSkipFails + 1);
4948
+ if (this._autoResetThreshold > 0 && prevSkipFails + 1 >= this._autoResetThreshold) {
4949
+ this.resetRatchet(peerForSkip);
4950
+ resetPeers.add(peerForSkip);
4951
+ this._onRatchetReset?.(peerForSkip, prevSkipFails + 1);
4952
+ }
4883
4953
  continue;
4884
4954
  } else {
4885
4955
  let ck = state.recvChainKey;
@@ -4907,8 +4977,23 @@ var VoidlyAgent = class _VoidlyAgent {
4907
4977
  }
4908
4978
  if (!rawPlaintext) {
4909
4979
  this._decryptFailCount++;
4980
+ failedIds.push(msg.id);
4981
+ const senderForFail = msg.from || "unknown";
4982
+ const prevFails = this._peerDecryptFails.get(senderForFail) || 0;
4983
+ this._peerDecryptFails.set(senderForFail, prevFails + 1);
4984
+ if (this._autoResetThreshold > 0 && prevFails + 1 >= this._autoResetThreshold) {
4985
+ this.resetRatchet(senderForFail);
4986
+ resetPeers.add(senderForFail);
4987
+ this._onRatchetReset?.(senderForFail, prevFails + 1);
4988
+ }
4910
4989
  continue;
4911
4990
  }
4991
+ {
4992
+ const senderForSuccess = msg.from || "unknown";
4993
+ if (this._peerDecryptFails.has(senderForSuccess)) {
4994
+ this._peerDecryptFails.delete(senderForSuccess);
4995
+ }
4996
+ }
4912
4997
  let plaintextBytes = rawPlaintext;
4913
4998
  let wasPadded = false;
4914
4999
  let wasSealed = false;
@@ -4973,6 +5058,10 @@ var VoidlyAgent = class _VoidlyAgent {
4973
5058
  }
4974
5059
  if (this.requireSignatures && !signatureValid) {
4975
5060
  this._decryptFailCount++;
5061
+ failedIds.push(msg.id);
5062
+ const peerForSig = msg.from || "unknown";
5063
+ const prevSigFails = this._peerDecryptFails.get(peerForSig) || 0;
5064
+ this._peerDecryptFails.set(peerForSig, prevSigFails + 1);
4976
5065
  continue;
4977
5066
  }
4978
5067
  this._seenMessageIds.add(msg.id);
@@ -4999,13 +5088,22 @@ var VoidlyAgent = class _VoidlyAgent {
4999
5088
  });
5000
5089
  } catch {
5001
5090
  this._decryptFailCount++;
5091
+ failedIds.push(msg.id);
5092
+ const peerForCatch = msg.from || "unknown";
5093
+ const prevCatchFails = this._peerDecryptFails.get(peerForCatch) || 0;
5094
+ this._peerDecryptFails.set(peerForCatch, prevCatchFails + 1);
5095
+ if (this._autoResetThreshold > 0 && prevCatchFails + 1 >= this._autoResetThreshold) {
5096
+ this.resetRatchet(peerForCatch);
5097
+ resetPeers.add(peerForCatch);
5098
+ this._onRatchetReset?.(peerForCatch, prevCatchFails + 1);
5099
+ }
5002
5100
  }
5003
5101
  }
5004
- if (decrypted.length > 0) {
5102
+ if (decrypted.length > 0 || this._peerDecryptFails.size > 0) {
5005
5103
  this._persistRatchetState().catch(() => {
5006
5104
  });
5007
5105
  }
5008
- return decrypted;
5106
+ return { decrypted, failedIds };
5009
5107
  }
5010
5108
  // ─── Message Management ─────────────────────────────────────────────────────
5011
5109
  /**
@@ -6361,7 +6459,13 @@ var VoidlyAgent = class _VoidlyAgent {
6361
6459
  if (eventType === "message" && dataStr) {
6362
6460
  try {
6363
6461
  const rawMsg = JSON.parse(dataStr);
6364
- const decrypted = await this._decryptMessages([rawMsg]);
6462
+ const { decrypted, failedIds } = await this._decryptMessages([rawMsg]);
6463
+ for (const id of failedIds) {
6464
+ try {
6465
+ await this.markRead(id);
6466
+ } catch {
6467
+ }
6468
+ }
6365
6469
  if (decrypted.length > 0) {
6366
6470
  consecutiveEmpty = 0;
6367
6471
  sseFailures = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voidly/agent-sdk",
3
- "version": "3.2.7",
3
+ "version": "3.3.1",
4
4
  "description": "E2E encrypted agent-to-agent communication SDK — Double Ratchet, X3DH, deniable auth, ML-KEM-768 post-quantum, SSE streaming, ratchet persistence, multi-relay federation",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",