@agentvault/agentvault 0.19.34 → 0.19.36

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.js CHANGED
@@ -44743,7 +44743,7 @@ var init_ratchet = __esm({
44743
44743
  async "../crypto/dist/ratchet.js"() {
44744
44744
  "use strict";
44745
44745
  await init_libsodium_wrappers();
44746
- MAX_SKIP = 100;
44746
+ MAX_SKIP = 1e3;
44747
44747
  CHAIN_KEY_SEED = new Uint8Array([1]);
44748
44748
  MSG_KEY_SEED = new Uint8Array([2]);
44749
44749
  HEADER_KEY_SEED = new Uint8Array([3]);
@@ -47236,6 +47236,12 @@ var init_channel = __esm({
47236
47236
  this._primaryConversationId = this._persisted.primaryConversationId;
47237
47237
  this._fingerprint = this._persisted.fingerprint;
47238
47238
  this._lastInboundRoomId = this._persisted.lastInboundRoomId;
47239
+ if (this._persisted.seenMessageIds) {
47240
+ this._seenMessageIds = new Set(this._persisted.seenMessageIds.slice(-_SecureChannel.SEEN_MSG_MAX));
47241
+ }
47242
+ if (this._persisted.seenA2AMessageIds) {
47243
+ this._a2aSeenMessageIds = new Set(this._persisted.seenA2AMessageIds.slice(-_SecureChannel.A2A_SEEN_MAX));
47244
+ }
47239
47245
  for (const [convId, sessionData] of Object.entries(
47240
47246
  this._persisted.sessions
47241
47247
  )) {
@@ -47244,7 +47250,8 @@ var init_channel = __esm({
47244
47250
  this._sessions.set(convId, {
47245
47251
  ownerDeviceId: sessionData.ownerDeviceId,
47246
47252
  ratchet,
47247
- activated: sessionData.activated ?? false
47253
+ activated: sessionData.activated ?? false,
47254
+ epoch: sessionData.epoch
47248
47255
  });
47249
47256
  }
47250
47257
  }
@@ -47283,7 +47290,8 @@ var init_channel = __esm({
47283
47290
  this._sessions.set(convId, {
47284
47291
  ownerDeviceId: sd.ownerDeviceId,
47285
47292
  ratchet: DoubleRatchet.deserialize(sd.ratchetState),
47286
- activated: sd.activated ?? false
47293
+ activated: sd.activated ?? false,
47294
+ epoch: sd.epoch
47287
47295
  });
47288
47296
  }
47289
47297
  }
@@ -47382,6 +47390,7 @@ var init_channel = __esm({
47382
47390
  }
47383
47391
  const messageGroupId = randomUUID2();
47384
47392
  let sentCount = 0;
47393
+ const pendingWsSends = [];
47385
47394
  for (const [convId, session] of this._sessions) {
47386
47395
  if (!session.activated) continue;
47387
47396
  if (roomConvIds.has(convId)) continue;
@@ -47414,7 +47423,7 @@ var init_channel = __esm({
47414
47423
  if (this._persisted?.hubId) {
47415
47424
  payload.sender_hub_id = this._persisted.hubId;
47416
47425
  }
47417
- this._ws.send(
47426
+ pendingWsSends.push(
47418
47427
  JSON.stringify({
47419
47428
  event: "message",
47420
47429
  data: payload
@@ -47440,6 +47449,9 @@ var init_channel = __esm({
47440
47449
  console.warn("[SecureChannel] send() delivered to 0 sessions (all skipped or failed)");
47441
47450
  }
47442
47451
  await this._persistState();
47452
+ for (const frame of pendingWsSends) {
47453
+ this._ws.send(frame);
47454
+ }
47443
47455
  }
47444
47456
  /**
47445
47457
  * Send a typing indicator to all owner devices.
@@ -47619,13 +47631,15 @@ var init_channel = __esm({
47619
47631
  this._sessions.set(conv.id, {
47620
47632
  ownerDeviceId: otherDeviceId,
47621
47633
  ratchet,
47622
- activated: isInitiator
47634
+ activated: isInitiator,
47623
47635
  // initiator can send immediately
47636
+ epoch: 1
47624
47637
  });
47625
47638
  this._persisted.sessions[conv.id] = {
47626
47639
  ownerDeviceId: otherDeviceId,
47627
47640
  ratchetState: ratchet.serialize(),
47628
- activated: isInitiator
47641
+ activated: isInitiator,
47642
+ epoch: 1
47629
47643
  };
47630
47644
  conversationIds.push(conv.id);
47631
47645
  console.log(
@@ -47718,6 +47732,7 @@ var init_channel = __esm({
47718
47732
  if (recipients.length === 0) {
47719
47733
  throw new Error("No active sessions in room");
47720
47734
  }
47735
+ await this._persistState();
47721
47736
  if (this._state === "ready" && this._ws) {
47722
47737
  this._ws.send(
47723
47738
  JSON.stringify({
@@ -47757,7 +47772,6 @@ var init_channel = __esm({
47757
47772
  throw new Error(`Failed to send room message: ${err}`);
47758
47773
  }
47759
47774
  }
47760
- await this._persistState();
47761
47775
  }
47762
47776
  /**
47763
47777
  * Leave a room: remove sessions and persisted room state.
@@ -47856,11 +47870,12 @@ var init_channel = __esm({
47856
47870
  attachment: attachMeta
47857
47871
  });
47858
47872
  const messageGroupId = randomUUID2();
47873
+ const pendingWsSends = [];
47859
47874
  for (const [convId, session] of this._sessions) {
47860
47875
  if (!session.activated) continue;
47861
47876
  const encrypted = session.ratchet.encrypt(envelope);
47862
47877
  const transport = encryptedMessageToTransport(encrypted);
47863
- this._ws.send(
47878
+ pendingWsSends.push(
47864
47879
  JSON.stringify({
47865
47880
  event: "message",
47866
47881
  data: {
@@ -47874,6 +47889,9 @@ var init_channel = __esm({
47874
47889
  );
47875
47890
  }
47876
47891
  await this._persistState();
47892
+ for (const frame of pendingWsSends) {
47893
+ this._ws.send(frame);
47894
+ }
47877
47895
  }
47878
47896
  async sendActionConfirmation(confirmation) {
47879
47897
  const envelope = {
@@ -48716,12 +48734,14 @@ var init_channel = __esm({
48716
48734
  this._sessions.set(conv.conversation_id, {
48717
48735
  ownerDeviceId: conv.owner_device_id,
48718
48736
  ratchet,
48719
- activated: false
48737
+ activated: false,
48720
48738
  // Wait for owner's first message before sending to this session
48739
+ epoch: 1
48721
48740
  });
48722
48741
  sessions[conv.conversation_id] = {
48723
48742
  ownerDeviceId: conv.owner_device_id,
48724
- ratchetState: ratchet.serialize()
48743
+ ratchetState: ratchet.serialize(),
48744
+ epoch: 1
48725
48745
  };
48726
48746
  console.log(
48727
48747
  `[SecureChannel] Session initialized for conv ${conv.conversation_id.slice(0, 8)}... (owner ${conv.owner_device_id.slice(0, 8)}..., primary=${conv.is_primary})`
@@ -49220,8 +49240,9 @@ var init_channel = __esm({
49220
49240
  ownerDeviceId: "",
49221
49241
  // A2A sessions don't have an owner device
49222
49242
  ratchetState: ratchet.serialize(),
49223
- activated: activatedRole === "initiator"
49243
+ activated: activatedRole === "initiator",
49224
49244
  // initiator can send immediately
49245
+ epoch: 1
49225
49246
  };
49226
49247
  channelEntry.role = activatedRole;
49227
49248
  if (convId) channelEntry.conversationId = convId;
@@ -49406,7 +49427,8 @@ var init_channel = __esm({
49406
49427
  conversation_id: convId,
49407
49428
  reason: "no_session",
49408
49429
  identity_public_key: this._persisted.identityKeypair.publicKey,
49409
- ephemeral_public_key: this._persisted.ephemeralKeypair.publicKey
49430
+ ephemeral_public_key: this._persisted.ephemeralKeypair.publicKey,
49431
+ epoch: this._persisted.sessions[convId]?.epoch
49410
49432
  }
49411
49433
  })
49412
49434
  );
@@ -49445,7 +49467,8 @@ var init_channel = __esm({
49445
49467
  conversation_id: convId,
49446
49468
  reason: "decrypt_failure",
49447
49469
  identity_public_key: idPubHex,
49448
- ephemeral_public_key: ephPubHex
49470
+ ephemeral_public_key: ephPubHex,
49471
+ epoch: this._persisted?.sessions[convId]?.epoch
49449
49472
  }
49450
49473
  })
49451
49474
  );
@@ -49690,11 +49713,12 @@ ${messageText}`;
49690
49713
  });
49691
49714
  this._appendHistory("agent", plaintext, topicId);
49692
49715
  const messageGroupId = randomUUID2();
49716
+ const pendingWsSends = [];
49693
49717
  for (const [convId, session] of this._sessions) {
49694
49718
  if (!session.activated) continue;
49695
49719
  const encrypted = session.ratchet.encrypt(envelope);
49696
49720
  const transport = encryptedMessageToTransport(encrypted);
49697
- this._ws.send(
49721
+ pendingWsSends.push(
49698
49722
  JSON.stringify({
49699
49723
  event: "message",
49700
49724
  data: {
@@ -49708,6 +49732,9 @@ ${messageText}`;
49708
49732
  );
49709
49733
  }
49710
49734
  await this._persistState();
49735
+ for (const frame of pendingWsSends) {
49736
+ this._ws.send(frame);
49737
+ }
49711
49738
  }
49712
49739
  /**
49713
49740
  * Relay an owner's message to all sibling sessions as encrypted sync messages.
@@ -49730,13 +49757,14 @@ ${messageText}`;
49730
49757
  ts: (/* @__PURE__ */ new Date()).toISOString(),
49731
49758
  topicId
49732
49759
  });
49760
+ const pendingWsSends = [];
49733
49761
  for (const [siblingConvId, siblingSession] of this._sessions) {
49734
49762
  if (siblingConvId === sourceConvId) continue;
49735
49763
  if (!siblingSession.activated) continue;
49736
49764
  if (roomConvIds.has(siblingConvId)) continue;
49737
49765
  const syncEncrypted = siblingSession.ratchet.encrypt(syncPayload);
49738
49766
  const syncTransport = encryptedMessageToTransport(syncEncrypted);
49739
- this._ws.send(
49767
+ pendingWsSends.push(
49740
49768
  JSON.stringify({
49741
49769
  event: "message",
49742
49770
  data: {
@@ -49747,6 +49775,10 @@ ${messageText}`;
49747
49775
  })
49748
49776
  );
49749
49777
  }
49778
+ await this._persistState();
49779
+ for (const frame of pendingWsSends) {
49780
+ this._ws.send(frame);
49781
+ }
49750
49782
  }
49751
49783
  /**
49752
49784
  * Resolve the agent's workspace directory.
@@ -49790,6 +49822,7 @@ ${messageText}`;
49790
49822
  const plaintext = JSON.stringify(payload);
49791
49823
  const encrypted = session.ratchet.encrypt(plaintext);
49792
49824
  const transport = encryptedMessageToTransport(encrypted);
49825
+ await this._persistState();
49793
49826
  this._ws.send(
49794
49827
  JSON.stringify({
49795
49828
  event: "message",
@@ -49800,7 +49833,6 @@ ${messageText}`;
49800
49833
  }
49801
49834
  })
49802
49835
  );
49803
- await this._persistState();
49804
49836
  }
49805
49837
  /**
49806
49838
  * Send stored message history to a newly-activated session.
@@ -49823,6 +49855,7 @@ ${messageText}`;
49823
49855
  });
49824
49856
  const encrypted = session.ratchet.encrypt(replayPayload);
49825
49857
  const transport = encryptedMessageToTransport(encrypted);
49858
+ await this._persistState();
49826
49859
  this._ws.send(
49827
49860
  JSON.stringify({
49828
49861
  event: "message",
@@ -49869,13 +49902,15 @@ ${messageText}`;
49869
49902
  this._sessions.set(event.conversation_id, {
49870
49903
  ownerDeviceId: event.owner_device_id,
49871
49904
  ratchet,
49872
- activated: false
49905
+ activated: false,
49873
49906
  // Wait for owner's first message
49907
+ epoch: 1
49874
49908
  });
49875
49909
  this._persisted.sessions[event.conversation_id] = {
49876
49910
  ownerDeviceId: event.owner_device_id,
49877
49911
  ratchetState: ratchet.serialize(),
49878
- activated: false
49912
+ activated: false,
49913
+ epoch: 1
49879
49914
  };
49880
49915
  if (event.owner_hub_id) {
49881
49916
  this._persisted.ownerHubId = event.owner_hub_id;
@@ -49922,17 +49957,21 @@ ${messageText}`;
49922
49957
  keyType: "ed25519"
49923
49958
  }, hexToBytes(data.identity_public_key));
49924
49959
  const existingSession = this._sessions.get(convId);
49960
+ const existingPersisted = this._persisted.sessions[convId];
49925
49961
  const ownerDeviceId = data.sender_device_id ?? existingSession?.ownerDeviceId ?? "";
49962
+ const newEpoch = (existingPersisted?.epoch ?? existingSession?.epoch ?? 0) + 1;
49926
49963
  this._sessions.set(convId, {
49927
49964
  ownerDeviceId,
49928
49965
  ratchet,
49929
- activated: false
49966
+ activated: false,
49930
49967
  // Wait for owner's encrypted session_init
49968
+ epoch: newEpoch
49931
49969
  });
49932
49970
  this._persisted.sessions[convId] = {
49933
49971
  ownerDeviceId,
49934
49972
  ratchetState: ratchet.serialize(),
49935
- activated: false
49973
+ activated: false,
49974
+ epoch: newEpoch
49936
49975
  };
49937
49976
  await this._persistState();
49938
49977
  if (this._ws) {
@@ -49942,7 +49981,8 @@ ${messageText}`;
49942
49981
  data: {
49943
49982
  conversation_id: convId,
49944
49983
  identity_public_key: identity.publicKey,
49945
- ephemeral_public_key: ephemeral.publicKey
49984
+ ephemeral_public_key: ephemeral.publicKey,
49985
+ epoch: newEpoch
49946
49986
  }
49947
49987
  })
49948
49988
  );
@@ -49956,6 +49996,32 @@ ${messageText}`;
49956
49996
  this.emit("error", err);
49957
49997
  }
49958
49998
  }
49999
+ /**
50000
+ * Send a resync_request for a room conversation if the cooldown has elapsed.
50001
+ * Shared by all room ratchet failure paths to avoid code duplication.
50002
+ */
50003
+ _sendRoomResyncIfCooled(convId, reason, session) {
50004
+ const RESYNC_COOLDOWN_MS = 5 * 60 * 1e3;
50005
+ const lastResync = this._lastResyncRequest.get(convId) ?? 0;
50006
+ if (Date.now() - lastResync > RESYNC_COOLDOWN_MS && this._ws && this._persisted) {
50007
+ this._lastResyncRequest.set(convId, Date.now());
50008
+ this._ws.send(
50009
+ JSON.stringify({
50010
+ event: "resync_request",
50011
+ data: {
50012
+ conversation_id: convId,
50013
+ reason,
50014
+ identity_public_key: this._persisted.identityKeypair.publicKey,
50015
+ ephemeral_public_key: this._persisted.ephemeralKeypair.publicKey,
50016
+ epoch: session?.epoch
50017
+ }
50018
+ })
50019
+ );
50020
+ console.log(
50021
+ `[SecureChannel] Room resync requested for conv ${convId.slice(0, 8)} (${reason})`
50022
+ );
50023
+ }
50024
+ }
49959
50025
  /**
49960
50026
  * Handle an incoming room message. Finds the pairwise conversation
49961
50027
  * for the sender, decrypts, and emits a room_message event.
@@ -50075,20 +50141,39 @@ ${messageText}`;
50075
50141
  console.log(
50076
50142
  `[SecureChannel] Room ratchet re-initialized for conv ${convId.slice(0, 8)}...`
50077
50143
  );
50078
- plaintext = session.ratchet.decrypt(encrypted);
50079
- session.activated = true;
50080
- if (this._persisted.sessions[convId]) {
50081
- this._persisted.sessions[convId].activated = true;
50144
+ const incomingMsgNum = encrypted.header.messageNumber;
50145
+ if (incomingMsgNum <= 5) {
50146
+ try {
50147
+ plaintext = session.ratchet.decrypt(encrypted);
50148
+ session.activated = true;
50149
+ if (this._persisted.sessions[convId]) {
50150
+ this._persisted.sessions[convId].activated = true;
50151
+ }
50152
+ await this._persistState();
50153
+ console.log(
50154
+ `[SecureChannel] Room session ${convId.slice(0, 8)}... re-activated after ratchet re-init`
50155
+ );
50156
+ } catch (retryErr) {
50157
+ console.warn(
50158
+ `[SecureChannel] Room re-init retry failed for conv ${convId.slice(0, 8)} (msgNum=${incomingMsgNum}):`,
50159
+ retryErr
50160
+ );
50161
+ this._sendRoomResyncIfCooled(convId, "room_reinit_retry_failed", session);
50162
+ return;
50163
+ }
50164
+ } else {
50165
+ console.log(
50166
+ `[SecureChannel] Room re-init: skipping message with msgNum=${incomingMsgNum} for conv ${convId.slice(0, 8)} (too far ahead for fresh ratchet)`
50167
+ );
50168
+ this._sendRoomResyncIfCooled(convId, "room_message_skip");
50169
+ return;
50082
50170
  }
50083
- await this._persistState();
50084
- console.log(
50085
- `[SecureChannel] Room session ${convId.slice(0, 8)}... re-activated after ratchet re-init`
50086
- );
50087
50171
  } catch (reinitErr) {
50088
50172
  console.error(
50089
50173
  `[SecureChannel] Room ratchet re-init failed for conv ${convId.slice(0, 8)}...:`,
50090
50174
  reinitErr
50091
50175
  );
50176
+ this._sendRoomResyncIfCooled(convId, "room_reinit_failed");
50092
50177
  return;
50093
50178
  }
50094
50179
  }
@@ -50262,13 +50347,14 @@ ${messageText}`;
50262
50347
  const dist = chain.getDistribution(this._deviceId);
50263
50348
  const distJson = JSON.stringify(dist);
50264
50349
  const alreadyDistributed = new Set(room.distributedTo ?? []);
50350
+ const pendingWsSends = [];
50265
50351
  for (const convId of room.conversationIds) {
50266
50352
  const session = this._sessions.get(convId);
50267
50353
  if (!session || alreadyDistributed.has(session.ownerDeviceId)) continue;
50268
50354
  const encrypted = session.ratchet.encrypt(distJson);
50269
50355
  const transport = encryptedMessageToTransport(encrypted);
50270
50356
  if (this._state === "ready" && this._ws) {
50271
- this._ws.send(
50357
+ pendingWsSends.push(
50272
50358
  JSON.stringify({
50273
50359
  event: "sender_key_distribution",
50274
50360
  data: {
@@ -50284,6 +50370,9 @@ ${messageText}`;
50284
50370
  }
50285
50371
  room.distributedTo = [...alreadyDistributed];
50286
50372
  await this._persistState();
50373
+ for (const frame of pendingWsSends) {
50374
+ this._ws.send(frame);
50375
+ }
50287
50376
  }
50288
50377
  /**
50289
50378
  * Handle an incoming sender_key_distribution event.
@@ -50481,8 +50570,10 @@ ${messageText}`;
50481
50570
  try {
50482
50571
  a2aPlaintext = ratchet.decrypt(encryptedMessage);
50483
50572
  } catch (decryptErr) {
50484
- console.error(`[SecureChannel] A2A decrypt failed \u2014 restoring ratchet state:`, decryptErr);
50573
+ console.error(`[SecureChannel] A2A decrypt failed \u2014 restoring ratchet, initiating rekey:`, decryptErr);
50485
50574
  channelEntry.session.ratchetState = ratchetSnapshot;
50575
+ await this._persistState();
50576
+ await this._initiateA2ARekey(channelId);
50486
50577
  return;
50487
50578
  }
50488
50579
  channelEntry.session.ratchetState = ratchet.serialize();
@@ -50524,24 +50615,9 @@ ${messageText}`;
50524
50615
  }
50525
50616
  } else {
50526
50617
  console.warn(
50527
- `[SecureChannel] Received encrypted A2A but no session for ${channelId.slice(0, 8)} \u2014 triggering key exchange`
50618
+ `[SecureChannel] Received encrypted A2A but no session for ${channelId.slice(0, 8)} \u2014 initiating rekey`
50528
50619
  );
50529
- const entry = this._persisted?.a2aChannels?.[channelId];
50530
- if (entry && !entry.pendingEphemeralPrivateKey && !entry.session) {
50531
- try {
50532
- const a2aEphemeral = await generateEphemeralKeypair();
50533
- const ephPubHex = bytesToHex(a2aEphemeral.publicKey);
50534
- entry.pendingEphemeralPrivateKey = bytesToHex(a2aEphemeral.privateKey);
50535
- this._ws?.send(JSON.stringify({
50536
- event: "a2a_key_exchange",
50537
- data: { channel_id: channelId, ephemeral_key: ephPubHex }
50538
- }));
50539
- await this._persistState();
50540
- console.log(`[SecureChannel] On-demand A2A key exchange for ${channelId.slice(0, 8)}`);
50541
- } catch (kxErr) {
50542
- console.warn(`[SecureChannel] On-demand key exchange failed:`, kxErr);
50543
- }
50544
- }
50620
+ await this._initiateA2ARekey(channelId);
50545
50621
  }
50546
50622
  } else {
50547
50623
  const a2aMsg = {
@@ -50558,6 +50634,51 @@ ${messageText}`;
50558
50634
  }
50559
50635
  }
50560
50636
  }
50637
+ /**
50638
+ * Initiate A2A channel rekey after decrypt failure.
50639
+ * Calls the backend to reset channel to approved, clears local session,
50640
+ * and submits a fresh ephemeral key for key exchange.
50641
+ */
50642
+ async _initiateA2ARekey(channelId) {
50643
+ const entry = this._persisted?.a2aChannels?.[channelId];
50644
+ if (!entry || !this._deviceJwt || !this._ws) return;
50645
+ const now = Date.now();
50646
+ const lastRekey = entry._lastRekeyAttempt ?? 0;
50647
+ if (now - lastRekey < 3e4) return;
50648
+ entry._lastRekeyAttempt = now;
50649
+ console.log(`[SecureChannel] Initiating A2A rekey for ${channelId.slice(0, 8)}...`);
50650
+ try {
50651
+ const res = await fetch(
50652
+ `${this.config.apiUrl}/api/v1/a2a/channels/${channelId}/rekey`,
50653
+ {
50654
+ method: "POST",
50655
+ headers: { Authorization: `Bearer ${this._deviceJwt}` }
50656
+ }
50657
+ );
50658
+ if (!res.ok) {
50659
+ const detail = await res.text();
50660
+ console.warn(`[SecureChannel] A2A rekey API failed (${res.status}): ${detail}`);
50661
+ return;
50662
+ }
50663
+ delete entry.session;
50664
+ delete entry.observerSession;
50665
+ delete entry.pendingEphemeralPrivateKey;
50666
+ delete entry.pendingObserverEphemeralPrivateKey;
50667
+ const a2aEphemeral = await generateEphemeralKeypair();
50668
+ entry.pendingEphemeralPrivateKey = bytesToHex(a2aEphemeral.privateKey);
50669
+ this._ws.send(JSON.stringify({
50670
+ event: "a2a_key_exchange",
50671
+ data: {
50672
+ channel_id: channelId,
50673
+ ephemeral_key: bytesToHex(a2aEphemeral.publicKey)
50674
+ }
50675
+ }));
50676
+ await this._persistState();
50677
+ console.log(`[SecureChannel] A2A rekey submitted for ${channelId.slice(0, 8)}`);
50678
+ } catch (err) {
50679
+ console.warn(`[SecureChannel] A2A rekey failed:`, err);
50680
+ }
50681
+ }
50561
50682
  /**
50562
50683
  * Paginated sync: fetch missed messages in pages of 200, up to 5 pages (1000 messages).
50563
50684
  * Tracks message IDs in _syncMessageIds to prevent duplicate processing from concurrent WS messages.
@@ -50978,6 +51099,8 @@ ${messageText}`;
50978
51099
  const hasA2AChannels = !!this._persisted.a2aChannels && Object.keys(this._persisted.a2aChannels).length > 0;
50979
51100
  if (!hasOwnerSessions && !hasA2AChannels) return;
50980
51101
  this._persisted.lastInboundRoomId = this._lastInboundRoomId;
51102
+ this._persisted.seenMessageIds = [...this._seenMessageIds].slice(-_SecureChannel.SEEN_MSG_MAX);
51103
+ this._persisted.seenA2AMessageIds = [...this._a2aSeenMessageIds].slice(-_SecureChannel.A2A_SEEN_MAX);
50981
51104
  for (const [convId, session] of this._sessions) {
50982
51105
  this._persisted.sessions[convId] = {
50983
51106
  ownerDeviceId: session.ownerDeviceId,