@agentunion/fastaun 0.5.20 → 0.5.21

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.
Files changed (107) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/_packed_docs/CHANGELOG.md +47 -0
  3. package/_packed_docs/INDEX.md +453 -440
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +164 -161
  5. package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/344/270/216/346/215/242owner/347/274/226/346/216/222/346/226/271/346/241/210.md +530 -0
  6. package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/345/256/214/346/225/264/350/256/276/350/256/241/344/270/216/345/256/236/346/226/275/350/256/241/345/210/222.md +685 -0
  7. package/_packed_docs/agent.md/working/agent.md-schema/344/270/216/347/255/276/345/217/221/351/252/214/347/255/276/346/265/201/347/250/213/346/276/204/346/270/205.md +349 -0
  8. package/_packed_docs/agent.md/working//346/234/254/345/234/260Docker/346/265/213/350/257/225Bug/346/270/205/345/215/225.md +40 -0
  9. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1433 -1392
  10. package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
  11. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +2 -0
  12. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +666 -658
  13. package/_packed_docs/sdk/0.5.21-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +368 -0
  14. package/_packed_docs/sdk/0.5.21-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +350 -0
  15. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
  16. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
  17. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
  18. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +4 -1
  19. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +42 -39
  20. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +147 -14
  21. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +48 -47
  22. package/_packed_docs/sdk/09-group-rpc-manual.md +5 -2
  23. package/_packed_docs/sdk/09-message-rpc-manual.md +250 -160
  24. package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +71 -0
  25. package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +88 -0
  26. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +134 -130
  27. package/_packed_docs/sdk/INDEX.md +239 -221
  28. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +930 -922
  29. package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +106 -43
  30. package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +180 -9
  31. package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -708
  32. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +480 -476
  33. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +1 -1
  34. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +2 -2
  35. package/dist/agent-md-schema.js +6 -4
  36. package/dist/agent-md-schema.js.map +1 -1
  37. package/dist/agent-md.d.ts +22 -2
  38. package/dist/agent-md.js +84 -67
  39. package/dist/agent-md.js.map +1 -1
  40. package/dist/aid-store.d.ts +13 -0
  41. package/dist/aid-store.js +50 -15
  42. package/dist/aid-store.js.map +1 -1
  43. package/dist/auth.js +4 -0
  44. package/dist/auth.js.map +1 -1
  45. package/dist/cert-utils.d.ts +1 -0
  46. package/dist/cert-utils.js +13 -0
  47. package/dist/cert-utils.js.map +1 -1
  48. package/dist/client/delivery.d.ts +59 -1
  49. package/dist/client/delivery.js +1018 -125
  50. package/dist/client/delivery.js.map +1 -1
  51. package/dist/client/rpc-pipeline.d.ts +4 -1
  52. package/dist/client/rpc-pipeline.js +123 -37
  53. package/dist/client/rpc-pipeline.js.map +1 -1
  54. package/dist/client/v2-e2ee.d.ts +12 -2
  55. package/dist/client/v2-e2ee.js +751 -294
  56. package/dist/client/v2-e2ee.js.map +1 -1
  57. package/dist/client.d.ts +26 -2
  58. package/dist/client.js +379 -63
  59. package/dist/client.js.map +1 -1
  60. package/dist/config.d.ts +4 -0
  61. package/dist/config.js +1 -0
  62. package/dist/config.js.map +1 -1
  63. package/dist/events.d.ts +18 -1
  64. package/dist/events.js +148 -9
  65. package/dist/events.js.map +1 -1
  66. package/dist/facades.d.ts +43 -1
  67. package/dist/facades.js +363 -7
  68. package/dist/facades.js.map +1 -1
  69. package/dist/index.d.ts +5 -3
  70. package/dist/index.js +1 -1
  71. package/dist/index.js.map +1 -1
  72. package/dist/keystore/aid-db.d.ts +6 -1
  73. package/dist/keystore/aid-db.js +184 -42
  74. package/dist/keystore/aid-db.js.map +1 -1
  75. package/dist/keystore/index.d.ts +14 -0
  76. package/dist/keystore/local-token-store.d.ts +9 -1
  77. package/dist/keystore/local-token-store.js +79 -14
  78. package/dist/keystore/local-token-store.js.map +1 -1
  79. package/dist/org-protocol.d.ts +45 -0
  80. package/dist/org-protocol.js +754 -11
  81. package/dist/org-protocol.js.map +1 -1
  82. package/dist/ownership-v21.d.ts +16 -3
  83. package/dist/ownership-v21.js +35 -17
  84. package/dist/ownership-v21.js.map +1 -1
  85. package/dist/ownership-workflow.d.ts +17 -0
  86. package/dist/ownership-workflow.js +511 -26
  87. package/dist/ownership-workflow.js.map +1 -1
  88. package/dist/secret-store/file-store.js +84 -0
  89. package/dist/secret-store/file-store.js.map +1 -1
  90. package/dist/seq-tracker.d.ts +1 -0
  91. package/dist/seq-tracker.js +2 -2
  92. package/dist/seq-tracker.js.map +1 -1
  93. package/dist/tools/cross-sdk-agent.d.ts +66 -3
  94. package/dist/tools/cross-sdk-agent.js +466 -36
  95. package/dist/tools/cross-sdk-agent.js.map +1 -1
  96. package/dist/transport.d.ts +3 -0
  97. package/dist/transport.js +98 -44
  98. package/dist/transport.js.map +1 -1
  99. package/dist/types.d.ts +1 -0
  100. package/dist/v2/e2ee/encrypt-group.js +1 -0
  101. package/dist/v2/e2ee/encrypt-group.js.map +1 -1
  102. package/dist/v2/e2ee/encrypt-p2p.js +1 -0
  103. package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
  104. package/dist/v2/e2ee/types.d.ts +1 -0
  105. package/dist/version.d.ts +1 -1
  106. package/dist/version.js +1 -1
  107. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import * as crypto from 'node:crypto';
2
+ import { _INTERNAL_MESSAGE_FLOW_LIMITS } from '../config.js';
2
3
  import { E2EEError, StateError, ValidationError } from '../errors.js';
3
4
  import { decryptMessage, encryptGroupMessage, encryptP2PMessage, } from '../v2/e2ee/index.js';
4
5
  import { V2Session } from '../v2/session/index.js';
@@ -349,6 +350,13 @@ export class V2E2EECoordinator {
349
350
  get client() {
350
351
  return this.runtime.client;
351
352
  }
353
+ async withApplicationPage(ns, source, operation, reservation, handlers) {
354
+ const delivery = this.client._delivery;
355
+ return typeof delivery.runApplicationPage === 'function'
356
+ ? await (handlers ? delivery.runApplicationPage(ns, source, operation, reservation, handlers)
357
+ : reservation ? delivery.runApplicationPage(ns, source, operation, reservation) : delivery.runApplicationPage(ns, source, operation))
358
+ : await operation();
359
+ }
352
360
  resetForwardCursorState() {
353
361
  this.pendingForwardCursors.clear();
354
362
  this.pendingForwardAcks.clear();
@@ -621,11 +629,13 @@ export class V2E2EECoordinator {
621
629
  return null;
622
630
  }
623
631
  }
624
- pendingSenderIKMessageKey(msg, groupId) {
632
+ pendingSenderIKMessageKey(msg, groupId, fromAid = '') {
625
633
  const client = this.client;
626
634
  const messageId = String(msg.message_id ?? '').trim();
627
635
  const seq = String(msg.seq ?? '').trim();
628
- const prefix = groupId ? `group:${groupId}` : `p2p:${client._aid ?? ''}`;
636
+ const prefix = groupId ? `group:${groupId}` : client._p2pPeerPullEnabled
637
+ ? client._delivery.peerNamespace(fromAid || String(msg.sender_aid ?? msg.from_aid ?? msg.from ?? ''))
638
+ : `p2p:${client._aid ?? ''}`;
629
639
  return `${prefix}:${messageId || seq || Math.random().toString(36).slice(2)}`;
630
640
  }
631
641
  pendingSenderIKFetchKey(fromAid, senderDeviceId, groupId) {
@@ -717,7 +727,7 @@ export class V2E2EECoordinator {
717
727
  return;
718
728
  const senderDeviceId = String(args.senderDeviceId ?? '');
719
729
  const groupId = String(args.groupId ?? '').trim();
720
- const messageKey = this.pendingSenderIKMessageKey(args.msg, groupId);
730
+ const messageKey = this.pendingSenderIKMessageKey(args.msg, groupId, fromAid);
721
731
  client._v2SenderIKPending.set(messageKey, {
722
732
  msg: { ...args.msg },
723
733
  fromAid,
@@ -833,10 +843,19 @@ export class V2E2EECoordinator {
833
843
  }
834
844
  plaintext = normalizeGroupMentionMode(plaintext, entry.msg, entry.msg.envelope_json);
835
845
  plaintext.group_id = entry.groupId;
846
+ plaintext.need_render = typeof entry.msg.need_render === 'boolean' ? entry.msg.need_render : plaintext.need_render !== false;
836
847
  await client._publishPulledMessage('group.message_created', `group:${entry.groupId}`, seq, plaintext, false, 'pending_retry');
837
848
  }
838
849
  else {
839
- await client._publishPulledMessage('message.received', `p2p:${client._aid ?? ''}`, seq, plaintext, false, 'pending_retry');
850
+ const ns = client._p2pPeerPullEnabled
851
+ ? await client._delivery.initializePeer(entry.fromAid) : `p2p:${client._aid ?? ''}`;
852
+ if (!generationIsCurrent())
853
+ return;
854
+ plaintext.sender_aid = entry.fromAid;
855
+ plaintext.need_render = typeof entry.msg.need_render === 'boolean' ? entry.msg.need_render : plaintext.need_render !== false;
856
+ await client._publishPulledMessage('message.received', ns, seq, plaintext, false, 'pending_retry');
857
+ if (client._p2pPeerPullEnabled && generationIsCurrent())
858
+ client._delivery.schedulePeerRecovery(entry.fromAid, seq);
840
859
  }
841
860
  if (generationIsCurrent())
842
861
  pending.delete(key);
@@ -852,6 +871,8 @@ export class V2E2EECoordinator {
852
871
  }
853
872
  async publishPendingV2VolatileMessage(rawMessage, plaintext, source) {
854
873
  const delivery = this.client._delivery;
874
+ const generation = Number(this.client._v2RuntimeGeneration ?? 0);
875
+ const context = JSON.stringify([this.client._aid, this.client._deviceId, this.client._slotId]);
855
876
  const claim = delivery.beginVolatileDelivery(rawMessage);
856
877
  if (claim === 'inflight')
857
878
  return;
@@ -865,8 +886,14 @@ export class V2E2EECoordinator {
865
886
  if (Object.prototype.hasOwnProperty.call(rawMessage, key))
866
887
  publicMessage[key] = rawMessage[key];
867
888
  }
889
+ publicMessage.need_render = typeof rawMessage.need_render === 'boolean' ? rawMessage.need_render : plaintext.need_render !== false;
868
890
  const appEvent = delivery.p2pAppEventForMessage(publicMessage);
869
891
  await this.client._publishAppEvent(appEvent.event, appEvent.payload, source);
892
+ if (this.client._p2pPeerPullEnabled) {
893
+ if (context !== JSON.stringify([this.client._aid, this.client._deviceId, this.client._slotId]))
894
+ throw new StateError('delivery invalidated');
895
+ this.ensureDeliveryCurrent(delivery.peerNamespace(String(rawMessage.sender_aid ?? rawMessage.from_aid ?? rawMessage.from ?? '')), generation);
896
+ }
870
897
  delivery.markVolatileSeen(rawMessage);
871
898
  }
872
899
  finally {
@@ -1040,6 +1067,7 @@ export class V2E2EECoordinator {
1040
1067
  timestamp: opts.timestamp,
1041
1068
  protectedHeaders: opts.protectedHeaders,
1042
1069
  context: opts.context,
1070
+ needRender: opts.needRender,
1043
1071
  });
1044
1072
  client._logMessageDebug('send-envelope', 'message.send.v2', 'message.send', {
1045
1073
  message_id: envelope.message_id,
@@ -1084,6 +1112,7 @@ export class V2E2EECoordinator {
1084
1112
  deviceId: targetDeviceId || undefined,
1085
1113
  protectedHeaders: opts?.protectedHeaders,
1086
1114
  context: opts?.context,
1115
+ needRender: opts?.needRender,
1087
1116
  useCache,
1088
1117
  });
1089
1118
  const result = await client.call('message.send', {
@@ -1093,6 +1122,7 @@ export class V2E2EECoordinator {
1093
1122
  ...(targetDeviceId ? { device_id: targetDeviceId } : {}),
1094
1123
  ...(opts?.ttl !== undefined ? { ttl: opts.ttl } : {}),
1095
1124
  encrypt: false,
1125
+ need_render: opts?.needRender ?? true,
1096
1126
  _skip_send_result_envelope: true,
1097
1127
  });
1098
1128
  client._clientLog.debug(`message.v2.send ok: to=${toAid}, use_cache=${useCache}, seq=${String((isJsonObject(result) ? result.seq : '') ?? '')}`);
@@ -1106,6 +1136,7 @@ export class V2E2EECoordinator {
1106
1136
  message_id: messageId,
1107
1137
  timestamp: opts?.timestamp,
1108
1138
  ttl: opts?.ttl,
1139
+ need_render: opts?.needRender ?? true,
1109
1140
  protected_headers: opts?.protectedHeaders,
1110
1141
  context: opts?.context,
1111
1142
  }, result, true);
@@ -1122,6 +1153,7 @@ export class V2E2EECoordinator {
1122
1153
  message_id: messageId,
1123
1154
  timestamp: opts?.timestamp,
1124
1155
  ttl: opts?.ttl,
1156
+ need_render: opts?.needRender ?? true,
1125
1157
  protected_headers: opts?.protectedHeaders,
1126
1158
  context: opts?.context,
1127
1159
  }, result, true);
@@ -1137,12 +1169,13 @@ export class V2E2EECoordinator {
1137
1169
  history_error: { code: 'decrypt_failed', message: reason },
1138
1170
  };
1139
1171
  }
1140
- ensureDeliveryCurrent(ns, generation) {
1172
+ ensureDeliveryCurrent(ns, generation, inlineGeneration) {
1141
1173
  if (Number(this.client._v2RuntimeGeneration ?? 0) !== generation
1174
+ || (inlineGeneration !== undefined && this.client._delivery.captureInlineAckGeneration() !== inlineGeneration)
1142
1175
  || this.client._rpcPipeline?.isPullNamespaceCurrent?.(ns) === false)
1143
1176
  throw new StateError('pull invalidated');
1144
1177
  }
1145
- async decodeP2PWindowMessage(msg, publish, history, source, orderedPublish = false) {
1178
+ async decodeP2PWindowMessage(msg, publish, history, source, orderedPublish = false, deferredEffects) {
1146
1179
  const client = this.client;
1147
1180
  const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
1148
1181
  let plaintext = null;
@@ -1173,11 +1206,11 @@ export class V2E2EECoordinator {
1173
1206
  else if (isJsonObject(payload)
1174
1207
  && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1175
1208
  msg = { ...msg, envelope_json: JSON.stringify(payload) };
1176
- plaintext = await client._decryptV2Message(msg, false, !history, true, true, undefined, false, source);
1209
+ plaintext = await client._decryptV2Message(msg, false, !history, true, true, undefined, false, source, ...(deferredEffects ? [deferredEffects] : []));
1177
1210
  }
1178
1211
  }
1179
1212
  else {
1180
- plaintext = await client._decryptV2Message(msg, false, !history, true, true, undefined, false, source);
1213
+ plaintext = await client._decryptV2Message(msg, false, !history, true, true, undefined, false, source, ...(deferredEffects ? [deferredEffects] : []));
1181
1214
  }
1182
1215
  if (!plaintext) {
1183
1216
  if (history)
@@ -1260,6 +1293,7 @@ export class V2E2EECoordinator {
1260
1293
  client._clientLog.warn(`Group Tail 单条消息解密失败,连续性证明仍保留: group=${groupId}, seq=${seq}`);
1261
1294
  return null;
1262
1295
  }
1296
+ plaintext.need_render = typeof msg.need_render === 'boolean' ? msg.need_render : plaintext.need_render !== false;
1263
1297
  if (publish && !history && client._delivery.isSelfSentGroupMessage?.(msg)) {
1264
1298
  return plaintext;
1265
1299
  }
@@ -1268,6 +1302,363 @@ export class V2E2EECoordinator {
1268
1302
  }
1269
1303
  return plaintext;
1270
1304
  }
1305
+ async pullPeerV2(senderAid, opts = {}) {
1306
+ const client = this.client;
1307
+ const generation = Number(client._v2RuntimeGeneration ?? 0);
1308
+ const inlineGeneration = client._delivery.captureInlineAckGeneration?.();
1309
+ await client._ensureV2SessionReady('message.pull');
1310
+ this.ensureDeliveryCurrent(client._delivery.peerNamespace(senderAid), generation, inlineGeneration);
1311
+ const ns = await client._delivery.initializePeer(senderAid);
1312
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1313
+ const limit = Number(opts.limit ?? _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize);
1314
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200)
1315
+ throw new ValidationError('peer limit must be between 1 and 200');
1316
+ const mode = opts.window_mode === 'tail' ? 'tail' : 'forward';
1317
+ const source = mode === 'tail' ? 'tail' : 'pull';
1318
+ if (mode === 'tail' && (opts.force === true || 'ack_up_to_seq' in opts))
1319
+ throw new ValidationError('Tail does not accept force or ACK');
1320
+ const budget = opts.budget ?? { remaining: Math.max(1, Number(opts.max_pages ?? 100)) };
1321
+ let after = opts.after_seq === undefined ? client._seqTracker.getContiguousSeq(ns) : strictWindowSeq(opts.after_seq, 'after_seq');
1322
+ if (opts.force !== true && after > client._seqTracker.getContiguousSeq(ns))
1323
+ throw new ValidationError('after_seq cannot exceed the peer delivery cursor');
1324
+ let volatileCursor = String(opts.after_volatile_cursor ?? client._delivery.peerVolatilePullCursor(senderAid));
1325
+ const decoded = [];
1326
+ let complete = false;
1327
+ let head = after;
1328
+ let pages = 0;
1329
+ let durableAck = 0;
1330
+ let initializedAck = false;
1331
+ let confirmedAck = 0;
1332
+ const confirmAck = (seq) => {
1333
+ confirmedAck = Math.max(confirmedAck, Math.min(seq, durableAck));
1334
+ this.clearForwardAck(ns, confirmedAck);
1335
+ };
1336
+ const prefetched = [];
1337
+ const stopping = new AbortController();
1338
+ const validatePage = (response, requestAfter, committedAck, preparing = false) => {
1339
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1340
+ this.validatePeerResponse(response, senderAid);
1341
+ const rows = Array.isArray(response.messages) ? response.messages : null;
1342
+ if (!rows || rows.length > limit)
1343
+ throw new ValidationError('invalid peer messages page');
1344
+ const persistent = rows.filter((row) => row.storage !== 'volatile');
1345
+ const volatile = rows.filter((row) => row.storage === 'volatile');
1346
+ const end = strictWindowSeq(response.covered_through_seq, 'covered_through_seq');
1347
+ const pageHead = strictWindowSeq(response.head_seq, 'head_seq');
1348
+ let start;
1349
+ if (mode === 'tail') {
1350
+ if (response.window_mode !== 'tail')
1351
+ throw new ValidationError('peer Tail response mode mismatch');
1352
+ start = strictWindowSeq(response.window_start_seq, 'window_start_seq');
1353
+ if (start > end || end !== pageHead || (end > requestAfter && start < requestAfter + 1))
1354
+ throw new ValidationError('invalid peer Tail proof');
1355
+ }
1356
+ else {
1357
+ start = strictWindowSeq(response.scan_after_seq, 'scan_after_seq');
1358
+ const retention = safeNonNegativeSeq(response.retention_floor_seq) ?? 0;
1359
+ if (start < requestAfter || (!preparing && start > Math.max(requestAfter, retention, opts.force === true ? 0 : committedAck))
1360
+ || end < start || end > pageHead || response.next_after_seq !== end)
1361
+ throw new ValidationError('invalid peer Forward proof');
1362
+ if (typeof response.persistent_has_more !== 'boolean' || typeof response.volatile_has_more !== 'boolean'
1363
+ || response.has_more !== (response.persistent_has_more || response.volatile_has_more)
1364
+ || (!response.persistent_has_more && end < pageHead))
1365
+ throw new ValidationError('invalid peer continuation proof');
1366
+ }
1367
+ let previous = mode === 'tail' ? start - 1 : start;
1368
+ for (const row of persistent) {
1369
+ const seq = strictWindowSeq(row.seq, 'messages[].seq', true);
1370
+ if (seq <= previous || seq > end || String(row.sender_aid ?? row.from_aid ?? row.from ?? '') !== senderAid)
1371
+ throw new ValidationError('peer message is outside proof or sender');
1372
+ previous = seq;
1373
+ }
1374
+ if (mode === 'forward' && opts.force !== true)
1375
+ confirmAck(safeNonNegativeSeq(response.server_ack_seq) ?? 0);
1376
+ return { persistent, volatile, end, pageHead, start };
1377
+ };
1378
+ const decodeRows = async (rows) => {
1379
+ const prepared = { messages: [], effects: [] };
1380
+ try {
1381
+ for (const row of rows) {
1382
+ if (stopping.signal.aborted)
1383
+ throw new StateError('pull invalidated');
1384
+ const plaintext = await this.decodeP2PWindowMessage(row, false, false, source, false, prepared.effects);
1385
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1386
+ if (!plaintext)
1387
+ throw new E2EEError('peer message could not be decrypted');
1388
+ plaintext.sender_aid = senderAid;
1389
+ plaintext.need_render = typeof row.need_render === 'boolean' ? row.need_render : plaintext.need_render !== false;
1390
+ prepared.messages.push(plaintext);
1391
+ }
1392
+ }
1393
+ catch (error) {
1394
+ prepared.error = error;
1395
+ }
1396
+ return prepared;
1397
+ };
1398
+ const fetchPage = async (requestAfter, requestCursor) => {
1399
+ const key = `${ns}|mode=${mode}|after=${requestAfter}|volatile=${requestCursor}|force=${Boolean(opts.force)}|limit=${limit}`;
1400
+ const request = (cursor) => client._runPullSerialized(key, () => client._callRawV2Rpc('message.v2.pull', {
1401
+ cursor_scope: 'peer', sender_aid: senderAid, device_id: client._deviceId, slot_id: client._slotId,
1402
+ after_seq: requestAfter, limit,
1403
+ ...(mode === 'forward' && opts.force !== true && durableAck > 0 ? { ack_up_to_seq: durableAck } : {}),
1404
+ ...(mode === 'tail' ? { window_mode: 'tail' } : {}),
1405
+ ...(opts.force === true ? { force: true } : {}),
1406
+ ...(cursor ? { after_volatile_cursor: cursor } : {}),
1407
+ ...(opts.background ? { _rpc_background: true } : { _rpc_foreground: true }),
1408
+ }, key), Boolean(opts.background));
1409
+ const reservation = await client._dispatcher?._reserveMessageCapacity?.(limit, stopping.signal);
1410
+ try {
1411
+ if (stopping.signal.aborted)
1412
+ throw new StateError('pull invalidated');
1413
+ try {
1414
+ return { response: await request(requestCursor), reservation };
1415
+ }
1416
+ catch (error) {
1417
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1418
+ if (!requestCursor || !String(error instanceof Error ? error.message : error).toLowerCase().includes('volatile cursor epoch mismatch'))
1419
+ throw error;
1420
+ client._delivery.setPeerVolatilePullCursor(senderAid, '');
1421
+ return { response: await request(''), reservation };
1422
+ }
1423
+ }
1424
+ catch (error) {
1425
+ reservation?.release();
1426
+ throw error;
1427
+ }
1428
+ };
1429
+ const prefetchPage = (requestAfter, cursor) => {
1430
+ const existing = prefetched.find((page) => page.after === requestAfter && page.cursor === cursor);
1431
+ if (existing)
1432
+ return existing;
1433
+ if (prefetched.length >= 2 || budget.remaining <= 0 || stopping.signal.aborted)
1434
+ return undefined;
1435
+ budget.remaining -= 1;
1436
+ const page = { after: requestAfter, cursor, response: fetchPage(requestAfter, cursor) };
1437
+ void page.response.catch(() => undefined);
1438
+ prefetched.push(page);
1439
+ return page;
1440
+ };
1441
+ const preparePage = (page) => {
1442
+ if (page.prepared)
1443
+ return;
1444
+ page.prepared = page.response.then(async (fetched) => {
1445
+ try {
1446
+ const proof = validatePage(fetched.response, page.after, durableAck, true);
1447
+ const handlers = client._delivery.snapshotApplicationHandlers?.();
1448
+ if (proof.volatile.length === 0 && proof.end > page.after && fetched.response.persistent_has_more === true) {
1449
+ prefetchPage(proof.end, page.cursor);
1450
+ }
1451
+ return { ...fetched, handlers, prepared: await decodeRows(proof.persistent) };
1452
+ }
1453
+ catch (error) {
1454
+ fetched.reservation?.release();
1455
+ throw error;
1456
+ }
1457
+ });
1458
+ void page.prepared.catch(() => undefined);
1459
+ };
1460
+ client._delivery.syncStarted(ns);
1461
+ let failed = false;
1462
+ try {
1463
+ while (budget.remaining > 0 || prefetched.length > 0) {
1464
+ const cached = prefetched.shift();
1465
+ if (!cached)
1466
+ budget.remaining -= 1;
1467
+ pages += 1;
1468
+ let requestedAfter = cached ? cached.after : after;
1469
+ const requestedVolatile = cached ? cached.cursor : volatileCursor;
1470
+ const page = await client._delivery.runPeerPageSerialized(ns, async () => {
1471
+ const committedAck = client._delivery.peerCommittedAck(ns);
1472
+ if (!initializedAck) {
1473
+ durableAck = committedAck;
1474
+ initializedAck = true;
1475
+ }
1476
+ if (!cached && mode === 'forward' && opts.force !== true)
1477
+ requestedAfter = Math.max(requestedAfter, committedAck);
1478
+ if (!cached)
1479
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1480
+ const fetched = await (cached ? cached.prepared ?? cached.response : fetchPage(requestedAfter, requestedVolatile));
1481
+ const { response, reservation } = fetched;
1482
+ try {
1483
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1484
+ const { persistent, volatile, end, pageHead, start } = validatePage(response, requestedAfter, committedAck);
1485
+ const handlers = fetched.handlers ?? client._delivery.snapshotApplicationHandlers?.();
1486
+ if (mode === 'forward' && opts.force !== true && volatile.length === 0 && end > requestedAfter
1487
+ && (response.persistent_has_more === true || response.has_more === true)) {
1488
+ const pending = prefetchPage(end, requestedVolatile);
1489
+ if (pending)
1490
+ preparePage(pending);
1491
+ }
1492
+ const messages = [];
1493
+ const prepared = fetched.prepared ?? await decodeRows(persistent);
1494
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1495
+ for (const effect of prepared.effects)
1496
+ effect();
1497
+ if ('error' in prepared)
1498
+ throw prepared.error;
1499
+ await this.withApplicationPage(ns, source, async () => {
1500
+ for (let index = 0; index < persistent.length; index += 1) {
1501
+ const row = persistent[index], plaintext = prepared.messages[index];
1502
+ const event = client._delivery.p2pAppEventForMessage(plaintext);
1503
+ await client._delivery.publishPulledMessage(event.event, ns, row.seq, event.payload, false, source);
1504
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1505
+ messages.push(plaintext);
1506
+ }
1507
+ if (volatile.length > 0) {
1508
+ for (const row of volatile)
1509
+ if (String(row.sender_aid ?? row.from_aid ?? row.from ?? '') !== senderAid)
1510
+ throw new ValidationError('volatile peer sender mismatch');
1511
+ messages.push(...await this.publishV2VolatileMessages(volatile, source, true));
1512
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1513
+ }
1514
+ }, reservation, handlers);
1515
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1516
+ if (!volatile.every((row) => client._delivery.isVolatileSeen(row)))
1517
+ throw new E2EEError('volatile peer delivery remains pending');
1518
+ const before = client._seqTracker.snapshotNamespace(ns);
1519
+ try {
1520
+ if (mode === 'tail') {
1521
+ client._seqTracker.commitTailWindow(ns, { windowStartSeq: start, headSeq: pageHead, coveredThroughSeq: end, pageLimit: limit, sparse: true });
1522
+ }
1523
+ else if (opts.force !== true) {
1524
+ client._seqTracker.onPullResult(ns, persistent, requestedAfter, end);
1525
+ }
1526
+ if (opts.force !== true) {
1527
+ await client._commitSeqTrackerState(ns);
1528
+ durableAck = Math.max(durableAck, client._seqTracker.getContiguousSeq(ns));
1529
+ }
1530
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1531
+ }
1532
+ catch (error) {
1533
+ if (Number(client._v2RuntimeGeneration ?? 0) === generation
1534
+ && (inlineGeneration === undefined || client._delivery.captureInlineAckGeneration() === inlineGeneration)
1535
+ && client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false)
1536
+ client._seqTracker._restoreNamespaceSnapshotPreservingPush(ns, before);
1537
+ throw error;
1538
+ }
1539
+ const ack = client._seqTracker.getContiguousSeq(ns);
1540
+ if (mode === 'forward' && opts.force !== true) {
1541
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1542
+ const next = safeNonNegativeSeq(response.next_pending_seq);
1543
+ client._delivery.consumePeerPullHint(ns, end, response.persistent_has_more === true && next !== null && next > end && next <= pageHead ? next : undefined);
1544
+ confirmAck(Math.max(confirmedAck, safeNonNegativeSeq(response.server_ack_seq) ?? 0));
1545
+ if (ack > confirmedAck)
1546
+ this.recordForwardAck(ns, ack);
1547
+ }
1548
+ return { response, messages, end, head: pageHead };
1549
+ }
1550
+ finally {
1551
+ reservation?.release();
1552
+ }
1553
+ });
1554
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1555
+ if (opts.retainMessages !== false)
1556
+ decoded.push(...page.messages);
1557
+ head = page.head;
1558
+ volatileCursor = client._delivery.peerVolatilePullCursor(senderAid);
1559
+ const more = page.response.persistent_has_more === true || page.response.volatile_has_more === true || page.response.has_more === true;
1560
+ complete = !more;
1561
+ client._delivery.syncProgress(ns, { rawCount: page.response.messages.length, pulledCount: page.messages.length, hasMore: more,
1562
+ currentSeq: client._seqTracker.getContiguousSeq(ns), targetSeq: head });
1563
+ after = page.end;
1564
+ if (mode === 'tail' || complete || (after <= requestedAfter && volatileCursor === requestedVolatile))
1565
+ break;
1566
+ }
1567
+ }
1568
+ catch (error) {
1569
+ failed = true;
1570
+ throw error;
1571
+ }
1572
+ finally {
1573
+ stopping.abort();
1574
+ for (const pending of prefetched) {
1575
+ try {
1576
+ (await (pending.prepared ?? pending.response)).reservation?.release();
1577
+ }
1578
+ catch { }
1579
+ }
1580
+ if (mode === 'forward' && opts.force !== true) {
1581
+ try {
1582
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1583
+ const pending = this.pendingForwardAck(ns, durableAck);
1584
+ if (pending > 0) {
1585
+ await this.ackPeerV2(senderAid, pending);
1586
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1587
+ confirmAck(pending);
1588
+ }
1589
+ }
1590
+ catch (error) {
1591
+ if (!failed)
1592
+ throw error;
1593
+ }
1594
+ }
1595
+ }
1596
+ const ack = client._seqTracker.getContiguousSeq(ns);
1597
+ if (!opts.background && opts.force !== true && (!complete || (mode === 'tail' && ack < head)))
1598
+ client._delivery.schedulePeerRecovery(senderAid, head, true);
1599
+ return { sender_aid: senderAid, messages: decoded, complete: mode === 'tail' ? ack >= head : complete,
1600
+ ack_seq: ack, head_seq: head, pages };
1601
+ }
1602
+ validatePeerResponse(response, senderAid) {
1603
+ const client = this.client;
1604
+ if (!response || response.cursor_scope !== 'peer' || response.sender_aid !== senderAid
1605
+ || (response.device_id !== undefined && response.device_id !== client._deviceId)
1606
+ || (response.slot_id !== undefined && response.slot_id !== client._slotId))
1607
+ throw new ValidationError('peer response scope mismatch');
1608
+ }
1609
+ async ackPeerV2(senderAid, upToSeq, volatileCursors = [], volatileIds = []) {
1610
+ const client = this.client;
1611
+ const generation = Number(client._v2RuntimeGeneration ?? 0);
1612
+ const inlineGeneration = client._delivery.captureInlineAckGeneration?.();
1613
+ const ns = await client._delivery.initializePeer(senderAid);
1614
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1615
+ const localAck = client._delivery.peerCommittedAck(ns);
1616
+ const requested = upToSeq === undefined ? (volatileCursors.length || volatileIds.length ? 0 : localAck) : strictWindowSeq(upToSeq, 'up_to_seq');
1617
+ const seq = Math.min(requested, localAck);
1618
+ if (!seq && !volatileCursors.length && !volatileIds.length)
1619
+ return { cursor_scope: 'peer', sender_aid: senderAid, peer_ack_seq: 0 };
1620
+ const result = await client._callRawV2Rpc('message.v2.ack', { cursor_scope: 'peer', sender_aid: senderAid,
1621
+ device_id: client._deviceId, slot_id: client._slotId,
1622
+ ...(seq ? { up_to_seq: seq } : {}), ...(volatileCursors.length ? { volatile_cursors: volatileCursors } : {}),
1623
+ ...(volatileIds.length ? { volatile_ids: volatileIds } : {}), _rpc_background: true });
1624
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1625
+ this.validatePeerResponse(result, senderAid);
1626
+ const actual = safeNonNegativeSeq(result.peer_ack_seq ?? result.effective_ack_seq ?? result.up_to_seq);
1627
+ if (seq > 0 && (actual === null || actual < seq))
1628
+ throw new E2EEError('peer ACK watermark is below requested');
1629
+ client._delivery.peerAckChanged();
1630
+ return result;
1631
+ }
1632
+ async peerHistoryV2(params) {
1633
+ const sender = String(params.sender_aid ?? '').trim();
1634
+ if (!sender)
1635
+ throw new ValidationError('peer History requires sender_aid');
1636
+ const client = this.client;
1637
+ const ns = await client._delivery.initializePeer(sender);
1638
+ const generation = Number(client._v2RuntimeGeneration ?? 0);
1639
+ const before = strictWindowSeq(params.before_seq, 'before_seq', true);
1640
+ const limit = Number(params.limit ?? _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize);
1641
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200)
1642
+ throw new ValidationError('peer limit must be between 1 and 200');
1643
+ const key = pullGateKeyForClient(client, 'message.history', { ...params, sender_aid: sender, before_seq: before, limit }, `${ns}|history|before=${before}|limit=${limit}`);
1644
+ const result = await client._runPullSerialized(key, () => client._callRawV2Rpc('message.history', { ...params, cursor_scope: 'peer', sender_aid: sender,
1645
+ before_seq: before, limit, device_id: client._deviceId, slot_id: client._slotId, _rpc_foreground: true }, key), false);
1646
+ this.ensureDeliveryCurrent(ns, generation);
1647
+ this.validatePeerResponse(result, sender);
1648
+ const rows = validateHistoryPage(result, before);
1649
+ if (rows.length > limit)
1650
+ throw new ValidationError('peer History exceeds limit');
1651
+ const messages = [];
1652
+ for (const row of rows) {
1653
+ if (String(row.sender_aid ?? row.from_aid ?? row.from ?? '') !== sender)
1654
+ throw new ValidationError('peer History sender mismatch');
1655
+ const plaintext = await this.decodeP2PWindowMessage(row, false, true, 'history');
1656
+ this.ensureDeliveryCurrent(ns, generation);
1657
+ if (plaintext)
1658
+ messages.push({ ...plaintext, need_render: typeof row.need_render === 'boolean' ? row.need_render : plaintext.need_render !== false });
1659
+ }
1660
+ return { ...result, messages };
1661
+ }
1271
1662
  async pullV2TailInternal(params, deferPublish = false) {
1272
1663
  const client = this.client;
1273
1664
  const generation = Number(client._v2RuntimeGeneration ?? 0);
@@ -1286,12 +1677,14 @@ export class V2E2EECoordinator {
1286
1677
  const result = await client._callRawV2Rpc('message.v2.pull', { window_mode: 'tail', after_seq: afterSeq, limit, _rpc_foreground: true }, pullGateKey);
1287
1678
  const page = validateTailPage(result, afterSeq, limit);
1288
1679
  const decoded = [];
1289
- for (const msg of page.messages) {
1290
- this.ensureDeliveryCurrent(ns, generation);
1291
- const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail', deferPublish);
1292
- if (plaintext)
1293
- decoded.push(plaintext);
1294
- }
1680
+ await this.withApplicationPage(ns, 'tail', async () => {
1681
+ for (const msg of page.messages) {
1682
+ this.ensureDeliveryCurrent(ns, generation);
1683
+ const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail', deferPublish);
1684
+ if (plaintext)
1685
+ decoded.push(plaintext);
1686
+ }
1687
+ });
1295
1688
  this.ensureDeliveryCurrent(ns, generation);
1296
1689
  if (ns) {
1297
1690
  client._seqTracker.commitTailWindow(ns, {
@@ -1522,10 +1915,6 @@ export class V2E2EECoordinator {
1522
1915
  pipelineRunning = false;
1523
1916
  pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
1524
1917
  }
1525
- if (volatileMessages.length > 0) {
1526
- decrypted.push(...await this.publishV2VolatileMessages(volatileMessages, 'pull', true));
1527
- volatileCursor = String(client._delivery.volatilePullCursor?.() ?? '').trim();
1528
- }
1529
1918
  const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
1530
1919
  const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1531
1920
  const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
@@ -1556,116 +1945,119 @@ export class V2E2EECoordinator {
1556
1945
  startPipeline(pageMaxSeq, volatileCursor);
1557
1946
  const deferredKeyFetches = new Map();
1558
1947
  let blockedSeq = 0;
1559
- const pageDecryptedBefore = decrypted.length;
1560
- for (const msg of messages) {
1561
- const seq = Number(msg.seq ?? 0);
1562
- if (!Number.isFinite(seq) || seq <= 0)
1563
- continue;
1564
- const version = String(msg.version ?? 'v2');
1565
- if (version === 'v1') {
1566
- const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
1567
- const legacyPayload = legacy.payload !== undefined ? legacy.payload : msg.payload;
1568
- const payloadType = isJsonObject(legacyPayload)
1569
- ? String(legacyPayload.type ?? '').trim()
1570
- : '';
1571
- if (legacyPayload !== undefined && legacyPayload !== null
1572
- && !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
1573
- const v1Msg = {
1574
- message_id: String(msg.message_id ?? ''),
1575
- from: String(msg.from_aid ?? ''),
1576
- to: String(legacy.to ?? client._aid ?? ''),
1577
- seq: msg.seq,
1578
- type: String(msg.type ?? ''),
1579
- timestamp: msg.t_server,
1580
- payload: legacyPayload,
1581
- encrypted: false,
1582
- };
1583
- const direction = String(msg.direction ?? '').trim();
1584
- if (direction)
1585
- v1Msg.direction = direction;
1586
- else if (String(msg.from_aid ?? '').trim() && String(msg.from_aid ?? '').trim() !== String(client._aid ?? '').trim()) {
1587
- v1Msg.direction = 'inbound';
1948
+ let pageDecryptedBefore = decrypted.length;
1949
+ const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
1950
+ const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
1951
+ const serverAckSeq = parsedServerAckSeq ?? 0;
1952
+ const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
1953
+ let commitTarget = pageContigBefore;
1954
+ await this.withApplicationPage(ns, 'pull', async () => {
1955
+ if (volatileMessages.length > 0) {
1956
+ decrypted.push(...await this.publishV2VolatileMessages(volatileMessages, 'pull', true));
1957
+ pageDecryptedBefore = decrypted.length;
1958
+ }
1959
+ for (const msg of messages) {
1960
+ const seq = Number(msg.seq ?? 0);
1961
+ if (!Number.isFinite(seq) || seq <= 0)
1962
+ continue;
1963
+ const version = String(msg.version ?? 'v2');
1964
+ if (version === 'v1') {
1965
+ const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
1966
+ const legacyPayload = legacy.payload !== undefined ? legacy.payload : msg.payload;
1967
+ const payloadType = isJsonObject(legacyPayload)
1968
+ ? String(legacyPayload.type ?? '').trim()
1969
+ : '';
1970
+ if (legacyPayload !== undefined && legacyPayload !== null
1971
+ && !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
1972
+ const v1Msg = {
1973
+ message_id: String(msg.message_id ?? ''),
1974
+ from: String(msg.from_aid ?? ''),
1975
+ to: String(legacy.to ?? client._aid ?? ''),
1976
+ seq: msg.seq,
1977
+ type: String(msg.type ?? ''),
1978
+ timestamp: msg.t_server,
1979
+ payload: legacyPayload,
1980
+ encrypted: false,
1981
+ };
1982
+ const direction = String(msg.direction ?? '').trim();
1983
+ if (direction)
1984
+ v1Msg.direction = direction;
1985
+ else if (String(msg.from_aid ?? '').trim() && String(msg.from_aid ?? '').trim() !== String(client._aid ?? '').trim()) {
1986
+ v1Msg.direction = 'inbound';
1987
+ }
1988
+ if (msg.sender_device_id !== undefined)
1989
+ v1Msg.sender_device_id = msg.sender_device_id;
1990
+ attachGatewayProximity(v1Msg, msg);
1991
+ const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
1992
+ if (ns) {
1993
+ await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
1994
+ }
1995
+ else {
1996
+ await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull');
1997
+ }
1998
+ decrypted.push(v1Msg);
1999
+ client._clientLog.debug(`message.v2.pull plaintext V1 delivered: seq=${seq}, ns=${ns || '<none>'}`);
1588
2000
  }
1589
- if (msg.sender_device_id !== undefined)
1590
- v1Msg.sender_device_id = msg.sender_device_id;
1591
- attachGatewayProximity(v1Msg, msg);
1592
- const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
1593
- if (ns) {
1594
- await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
2001
+ else if (isJsonObject(legacyPayload)
2002
+ && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
2003
+ const deferStatus = {};
2004
+ const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(legacyPayload) }, true, true, true, true, deferStatus, true, 'pull');
2005
+ if (deferStatus.deferred) {
2006
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2007
+ if (deferStatus.fromAid) {
2008
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2009
+ deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
2010
+ }
2011
+ }
2012
+ if (plaintext) {
2013
+ if (ns)
2014
+ await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
2015
+ else
2016
+ await client._publishAppEvent('message.received', plaintext, 'pull');
2017
+ decrypted.push(plaintext);
2018
+ }
1595
2019
  }
1596
2020
  else {
1597
- await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull');
2021
+ client._clientLog.debug(`message.v2.pull skipping V1 envelope seq=${seq} payload_type=${payloadType || '<none>'} (V1 E2EE removed)`);
1598
2022
  }
1599
- decrypted.push(v1Msg);
1600
- client._clientLog.debug(`message.v2.pull plaintext V1 delivered: seq=${seq}, ns=${ns || '<none>'}`);
2023
+ continue;
1601
2024
  }
1602
- else if (isJsonObject(legacyPayload)
1603
- && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1604
- const deferStatus = {};
1605
- const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(legacyPayload) }, true, true, true, true, deferStatus, true, 'pull');
1606
- if (deferStatus.deferred) {
1607
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1608
- if (deferStatus.fromAid) {
1609
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1610
- deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
1611
- }
1612
- }
1613
- if (plaintext) {
1614
- if (ns)
1615
- await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1616
- else
1617
- await client._publishAppEvent('message.received', plaintext, 'pull');
1618
- decrypted.push(plaintext);
1619
- }
2025
+ if (version !== 'v2') {
2026
+ client._clientLog.debug(`message.v2.pull skipping non-V2 row seq=${seq} version=${String(msg.version ?? '')}`);
2027
+ continue;
2028
+ }
2029
+ const spkId = String(msg.spk_id ?? '');
2030
+ if (spkId && client._v2Session && !client._v2Session.isCurrentSPK(spkId)) {
2031
+ client._v2Session.trackOldSPKMaxSeq(spkId, seq);
2032
+ }
2033
+ const deferStatus = {};
2034
+ const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
2035
+ if (deferStatus.deferred) {
2036
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2037
+ }
2038
+ if (deferStatus.deferred && deferStatus.fromAid) {
2039
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2040
+ deferredKeyFetches.set(key, {
2041
+ fromAid: deferStatus.fromAid,
2042
+ senderDeviceId: deferStatus.senderDeviceId ?? '',
2043
+ groupId: deferStatus.groupId ?? '',
2044
+ });
2045
+ }
2046
+ if (plaintext === null) {
2047
+ client._clientLog.debug(`message.v2.pull decrypt returned null: seq=${seq}, ns=${ns || '<none>'}`);
2048
+ continue;
2049
+ }
2050
+ if (ns) {
2051
+ await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1620
2052
  }
1621
2053
  else {
1622
- client._clientLog.debug(`message.v2.pull skipping V1 envelope seq=${seq} payload_type=${payloadType || '<none>'} (V1 E2EE removed)`);
2054
+ await client._publishAppEvent('message.received', plaintext, 'pull');
1623
2055
  }
1624
- continue;
1625
- }
1626
- if (version !== 'v2') {
1627
- client._clientLog.debug(`message.v2.pull skipping non-V2 row seq=${seq} version=${String(msg.version ?? '')}`);
1628
- continue;
1629
- }
1630
- const spkId = String(msg.spk_id ?? '');
1631
- if (spkId && client._v2Session && !client._v2Session.isCurrentSPK(spkId)) {
1632
- client._v2Session.trackOldSPKMaxSeq(spkId, seq);
1633
- }
1634
- const deferStatus = {};
1635
- const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
1636
- if (deferStatus.deferred) {
1637
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1638
- }
1639
- if (deferStatus.deferred && deferStatus.fromAid) {
1640
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1641
- deferredKeyFetches.set(key, {
1642
- fromAid: deferStatus.fromAid,
1643
- senderDeviceId: deferStatus.senderDeviceId ?? '',
1644
- groupId: deferStatus.groupId ?? '',
1645
- });
1646
- }
1647
- if (plaintext === null) {
1648
- client._clientLog.debug(`message.v2.pull decrypt returned null: seq=${seq}, ns=${ns || '<none>'}`);
1649
- continue;
2056
+ decrypted.push(plaintext);
2057
+ client._logMessageDebug('decrypt-ok', 'message.v2.pull', 'message.received', plaintext);
1650
2058
  }
1651
2059
  if (ns) {
1652
- await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1653
- }
1654
- else {
1655
- await client._publishAppEvent('message.received', plaintext, 'pull');
1656
- }
1657
- decrypted.push(plaintext);
1658
- client._logMessageDebug('decrypt-ok', 'message.v2.pull', 'message.received', plaintext);
1659
- }
1660
- const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
1661
- const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
1662
- const serverAckSeq = parsedServerAckSeq ?? 0;
1663
- const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
1664
- if (ns) {
1665
- const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, serverAckSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
1666
- let ackSeq = pageContigBefore;
1667
- let contigAdvanced = false;
1668
- try {
2060
+ commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, serverAckSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
1669
2061
  this.ensureDeliveryCurrent(ns, generation);
1670
2062
  const window = client._seqTracker.getSyncState?.(ns);
1671
2063
  const deliveryTarget = window && commitTarget >= window.tail - 1
@@ -1674,6 +2066,15 @@ export class V2E2EECoordinator {
1674
2066
  await client._drainOrderedMessages(ns, undefined, false, false, 'pull', deliveryTarget);
1675
2067
  }
1676
2068
  this.ensureDeliveryCurrent(ns, generation);
2069
+ }
2070
+ });
2071
+ if (volatileMessages.length > 0)
2072
+ volatileCursor = String(client._delivery.volatilePullCursor?.() ?? '').trim();
2073
+ if (ns) {
2074
+ let ackSeq = pageContigBefore;
2075
+ let contigAdvanced = false;
2076
+ try {
2077
+ this.ensureDeliveryCurrent(ns, generation);
1677
2078
  pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
1678
2079
  if (commitTarget > pageContigBefore) {
1679
2080
  trackerMutated = true;
@@ -1824,11 +2225,27 @@ export class V2E2EECoordinator {
1824
2225
  const client = this.client;
1825
2226
  if (!client._v2Session)
1826
2227
  return [];
2228
+ const generation = Number(client._v2RuntimeGeneration ?? 0);
2229
+ const context = JSON.stringify([client._aid, client._deviceId, client._slotId]);
2230
+ const ensureCurrent = () => {
2231
+ if (!client._p2pPeerPullEnabled)
2232
+ return;
2233
+ if (context !== JSON.stringify([client._aid, client._deviceId, client._slotId]))
2234
+ throw new StateError('delivery invalidated');
2235
+ const sender = String(messages[0]?.sender_aid ?? messages[0]?.from_aid ?? messages[0]?.from ?? '').trim();
2236
+ if (sender)
2237
+ this.ensureDeliveryCurrent(client._delivery.peerNamespace(sender), generation);
2238
+ };
1827
2239
  const processed = [];
1828
2240
  const plaintextMessages = [];
1829
2241
  let complete = true;
1830
2242
  for (const rawMessage of messages) {
1831
- const claim = client._delivery.beginVolatileDelivery(rawMessage);
2243
+ let claim = client._delivery.beginVolatileDelivery(rawMessage);
2244
+ if (claim === 'inflight' && typeof client._delivery.waitVolatileDelivery === 'function') {
2245
+ await client._delivery.waitVolatileDelivery(rawMessage);
2246
+ ensureCurrent();
2247
+ claim = client._delivery.beginVolatileDelivery(rawMessage);
2248
+ }
1832
2249
  if (claim === 'seen') {
1833
2250
  processed.push(rawMessage);
1834
2251
  continue;
@@ -1867,13 +2284,16 @@ export class V2E2EECoordinator {
1867
2284
  complete = false;
1868
2285
  continue;
1869
2286
  }
2287
+ ensureCurrent();
1870
2288
  const publicMessage = client._delivery.publicVolatileMessage(plaintext);
1871
2289
  for (const key of ['storage', 'volatile_cursor', 'created_at', 'expires_at', 'persist']) {
1872
2290
  if (Object.prototype.hasOwnProperty.call(rawMessage, key))
1873
2291
  publicMessage[key] = rawMessage[key];
1874
2292
  }
2293
+ publicMessage.need_render = typeof rawMessage.need_render === 'boolean' ? rawMessage.need_render : publicMessage.need_render !== false;
1875
2294
  const appEvent = client._delivery.p2pAppEventForMessage(publicMessage);
1876
2295
  await client._publishAppEvent(appEvent.event, appEvent.payload, source);
2296
+ ensureCurrent();
1877
2297
  client._delivery.markVolatileSeen(rawMessage);
1878
2298
  processed.push(rawMessage);
1879
2299
  plaintextMessages.push(publicMessage);
@@ -1883,6 +2303,7 @@ export class V2E2EECoordinator {
1883
2303
  }
1884
2304
  }
1885
2305
  if (processed.length > 0) {
2306
+ ensureCurrent();
1886
2307
  await client._delivery.ackVolatileMessages(processed, true, advanceCursor && complete && processed.length === messages.length);
1887
2308
  }
1888
2309
  return plaintextMessages;
@@ -1997,6 +2418,7 @@ export class V2E2EECoordinator {
1997
2418
  timestamp: opts?.timestamp,
1998
2419
  protectedHeaders: opts?.protectedHeaders,
1999
2420
  context: opts?.context,
2421
+ needRender: opts?.needRender,
2000
2422
  useCache,
2001
2423
  });
2002
2424
  const result = await client.call('group.v2.send', withExplicitGroupAid({
@@ -2013,6 +2435,7 @@ export class V2E2EECoordinator {
2013
2435
  payload,
2014
2436
  message_id: messageId,
2015
2437
  timestamp: opts?.timestamp,
2438
+ need_render: opts?.needRender ?? true,
2016
2439
  protected_headers: opts?.protectedHeaders,
2017
2440
  context: opts?.context,
2018
2441
  }, groupAid), result, true);
@@ -2028,6 +2451,7 @@ export class V2E2EECoordinator {
2028
2451
  payload,
2029
2452
  message_id: messageId,
2030
2453
  timestamp: opts?.timestamp,
2454
+ need_render: opts?.needRender ?? true,
2031
2455
  protected_headers: opts?.protectedHeaders,
2032
2456
  context: opts?.context,
2033
2457
  }, groupAid), result, true);
@@ -2152,6 +2576,7 @@ export class V2E2EECoordinator {
2152
2576
  timestamp: opts.timestamp,
2153
2577
  protectedHeaders: opts.protectedHeaders,
2154
2578
  context: opts.context,
2579
+ needRender: opts.needRender,
2155
2580
  }, stateCommitment);
2156
2581
  client._logMessageDebug('send-envelope', 'group.send.v2', 'group.send', {
2157
2582
  group_id: groupId,
@@ -2173,7 +2598,7 @@ export class V2E2EECoordinator {
2173
2598
  });
2174
2599
  return envelope;
2175
2600
  }
2176
- async pullGroupV2TailInternal(params, deferPublish = false) {
2601
+ async pullGroupV2TailInternal(params, deferPublish = false, namespaceLocked = false) {
2177
2602
  const client = this.client;
2178
2603
  const generation = Number(client._v2RuntimeGeneration ?? 0);
2179
2604
  if (params.window_mode !== 'tail')
@@ -2187,6 +2612,7 @@ export class V2E2EECoordinator {
2187
2612
  const afterSeq = strictWindowSeq(params.after_seq ?? 0, 'after_seq');
2188
2613
  const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
2189
2614
  const ns = `group:${groupId}`;
2615
+ const namespaceGeneration = client._delivery.captureSeqNamespaceGeneration?.(ns);
2190
2616
  const explicitCursorParams = isJsonObject(params._group_cursor_params)
2191
2617
  ? params._group_cursor_params : {};
2192
2618
  const cursorParams = Object.keys(explicitCursorParams).length > 0 ? explicitCursorParams : params;
@@ -2203,35 +2629,55 @@ export class V2E2EECoordinator {
2203
2629
  limit,
2204
2630
  _group_cursor_params: explicitCursorParams,
2205
2631
  }, `${ns}|tail|after=${afterSeq}|limit=${limit}`);
2206
- const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2632
+ if (!namespaceLocked)
2633
+ return await client._rpcPipeline.runPullNamespaceSerialized(pullGateKey, () => this.pullGroupV2TailInternal(params, deferPublish, true), false);
2634
+ const result = await client._runPullSerialized(pullGateKey, () => client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2207
2635
  group_id: groupId,
2208
2636
  window_mode: 'tail',
2209
2637
  after_seq: afterSeq,
2210
2638
  limit,
2211
2639
  _group_cursor_params: explicitCursorParams,
2212
2640
  _rpc_foreground: true,
2213
- }, groupAid), pullGateKey);
2641
+ }, groupAid), pullGateKey), false);
2214
2642
  const resultAid = String(result.group_aid ?? result.groupAid ?? '').trim();
2215
2643
  if (resultAid)
2216
2644
  groupAid = resultAid;
2217
2645
  const page = validateTailPage(result, afterSeq, limit);
2218
2646
  const decoded = [];
2219
- for (const msg of page.messages) {
2220
- this.ensureDeliveryCurrent(ns, generation);
2221
- client._logMessageDebug('tail-raw', 'group.v2.pull', 'group.message_created', msg);
2222
- const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, true, false, 'tail', deferPublish);
2223
- if (plaintext)
2224
- decoded.push(plaintext);
2225
- }
2226
- this.ensureDeliveryCurrent(ns, generation);
2227
- client._seqTracker.commitTailWindow(ns, {
2228
- windowStartSeq: page.windowStart,
2229
- headSeq: page.head,
2230
- coveredThroughSeq: page.head,
2231
- pageLimit: limit,
2647
+ await this.withApplicationPage(ns, 'tail', async () => {
2648
+ for (const msg of page.messages) {
2649
+ this.ensureDeliveryCurrent(ns, generation);
2650
+ client._logMessageDebug('tail-raw', 'group.v2.pull', 'group.message_created', msg);
2651
+ const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, true, false, 'tail', deferPublish);
2652
+ if (plaintext)
2653
+ decoded.push(plaintext);
2654
+ }
2232
2655
  });
2233
- client._persistSeq(ns);
2234
- await client._saveSeqTrackerState?.();
2656
+ this.ensureDeliveryCurrent(ns, generation);
2657
+ if (client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) === false)
2658
+ throw new Error('pull invalidated');
2659
+ client._delivery.rememberCommittedSeqState?.(ns);
2660
+ const snapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
2661
+ try {
2662
+ client._seqTracker.commitTailWindow(ns, {
2663
+ windowStartSeq: page.windowStart,
2664
+ headSeq: page.head,
2665
+ coveredThroughSeq: page.head,
2666
+ pageLimit: limit,
2667
+ });
2668
+ await client._commitSeqTrackerState(ns);
2669
+ if (client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) === false)
2670
+ throw new Error('pull invalidated');
2671
+ }
2672
+ catch (error) {
2673
+ if (Number(client._v2RuntimeGeneration ?? 0) === generation
2674
+ && client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) !== false
2675
+ && client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false) {
2676
+ (client._seqTracker._restoreNamespaceSnapshotPreservingPush ?? client._seqTracker.restoreNamespaceSnapshot)
2677
+ .call(client._seqTracker, ns, snapshot);
2678
+ }
2679
+ throw error;
2680
+ }
2235
2681
  this.ensureDeliveryCurrent(ns, generation);
2236
2682
  const ack = client._seqTracker.getContiguousSeq(ns);
2237
2683
  if (ownsCursor)
@@ -2253,12 +2699,13 @@ export class V2E2EECoordinator {
2253
2699
  let groupAid = String(params.group_aid ?? '').trim();
2254
2700
  const beforeSeq = strictWindowSeq(params.before_seq, 'before_seq', true);
2255
2701
  const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
2256
- const result = await client._callRawV2Rpc('group.history', withExplicitGroupAid({
2702
+ const key = pullGateKeyForClient(client, 'group.history', { group_id: groupId, before_seq: beforeSeq, limit }, `group:${groupId}|history|before=${beforeSeq}|limit=${limit}`);
2703
+ const result = await client._runPullSerialized(key, () => client._callRawV2Rpc('group.history', withExplicitGroupAid({
2257
2704
  group_id: groupId,
2258
2705
  before_seq: beforeSeq,
2259
2706
  limit,
2260
2707
  _rpc_foreground: true,
2261
- }, groupAid));
2708
+ }, groupAid), key), false);
2262
2709
  const resultAid = String(result.group_aid ?? result.groupAid ?? '').trim();
2263
2710
  if (resultAid)
2264
2711
  groupAid = resultAid;
@@ -2295,7 +2742,7 @@ export class V2E2EECoordinator {
2295
2742
  group_id: gid, window_mode: 'tail', after_seq: afterSeq, limit,
2296
2743
  _group_cursor_params: opts?.cursorParams,
2297
2744
  }, `${ns}|tail|after=${afterSeq}|limit=${limit}`);
2298
- return (await client._runPullSerialized(key, () => this.pullGroupV2Impl(gid, afterSeq, limit, {
2745
+ return (await client._rpcPipeline.runPullNamespaceSerialized(key, () => this.pullGroupV2Impl(gid, afterSeq, limit, {
2299
2746
  ...opts,
2300
2747
  gateLocked: true,
2301
2748
  }), false));
@@ -2308,7 +2755,7 @@ export class V2E2EECoordinator {
2308
2755
  window_mode: 'tail',
2309
2756
  after_seq: afterSeq,
2310
2757
  limit,
2311
- });
2758
+ }, false, true);
2312
2759
  return (Array.isArray(result.messages) ? result.messages : []);
2313
2760
  }
2314
2761
  if (!opts?.gateLocked) {
@@ -2316,20 +2763,17 @@ export class V2E2EECoordinator {
2316
2763
  group_id: gid, after_seq: afterSeq, force: opts?.force === true, limit,
2317
2764
  _group_cursor_params: opts?.cursorParams,
2318
2765
  }, `${ns}|forward|after=${afterSeq}|force=${opts?.force === true}|limit=${limit}`);
2319
- return await client._runPullSerialized(key, async () => this.pullGroupV2Impl(gid, afterSeq, limit, {
2766
+ return await client._rpcPipeline.runPullNamespaceSerialized(key, async () => this.pullGroupV2Impl(gid, afterSeq, limit, {
2320
2767
  ...(opts ?? {}),
2321
2768
  gateLocked: true,
2322
2769
  scheduleFollowup: true,
2323
- }), true);
2770
+ }), opts?.background !== false);
2324
2771
  }
2325
2772
  const decrypted = [];
2326
2773
  const wireGroupId = String(opts?.wireGroupId ?? groupId ?? '').trim() || gid;
2327
2774
  const cursorParams = opts?.cursorParams ?? {};
2328
2775
  const ownsCursor = opts?.ownsCursor !== false;
2329
- let pullGateKey = pullGateKeyForClient(client, 'group.v2.pull', {
2330
- group_id: gid, after_seq: afterSeq, force: opts?.force === true, limit,
2331
- _group_cursor_params: cursorParams,
2332
- }, `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
2776
+ const background = opts?.background !== false;
2333
2777
  let nextAfterSeq = (opts?.explicitAfterSeq || opts?.force)
2334
2778
  ? afterSeq
2335
2779
  : (afterSeq || client._seqTracker.getContiguousSeq(ns));
@@ -2340,6 +2784,21 @@ export class V2E2EECoordinator {
2340
2784
  let pageCount = 0;
2341
2785
  let lastAutoAckSeq = 0;
2342
2786
  let pendingAckSeq = deferredForwardAck;
2787
+ let committedAckSeq = ownsCursor ? client._seqTracker.getContiguousSeq(ns) : 0;
2788
+ const fetchPage = async (after, ack) => {
2789
+ const key = pullGateKeyForClient(client, 'group.v2.pull', {
2790
+ group_id: gid, after_seq: after, force: opts?.force === true, limit, _group_cursor_params: cursorParams,
2791
+ }, `${ns}|forward|after=${after}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
2792
+ return await client._runPullSerialized(key, async () => {
2793
+ this.ensureDeliveryCurrent(ns, generation);
2794
+ return await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2795
+ group_id: wireGroupId, after_seq: after, limit, ...cursorParams,
2796
+ ...(opts?.force ? { force: true } : {}),
2797
+ ...(ack > 0 ? { ack_up_to_seq: Math.min(ack, committedAckSeq) } : {}),
2798
+ _rpc_background: background, _rpc_foreground: !background,
2799
+ }, groupAid), key);
2800
+ }, background);
2801
+ };
2343
2802
  const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
2344
2803
  let pipelineRunning = false;
2345
2804
  let pipelineError = null;
@@ -2356,16 +2815,9 @@ export class V2E2EECoordinator {
2356
2815
  if (pipelineStopped)
2357
2816
  break;
2358
2817
  this.ensureDeliveryCurrent(ns, generation);
2359
- const ackUpToSeq = pendingAckSeq;
2818
+ const ackUpToSeq = Math.min(pendingAckSeq, committedAckSeq);
2360
2819
  const inlineAckGeneration = ackUpToSeq > 0 ? client._delivery.captureGroupInlineAckGeneration() : null;
2361
- const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2362
- group_id: wireGroupId,
2363
- after_seq: producerAfterSeq,
2364
- limit,
2365
- ...cursorParams,
2366
- ...(opts?.force ? { force: true } : {}),
2367
- ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
2368
- }, groupAid), pullGateKey);
2820
+ const result = await fetchPage(producerAfterSeq, ackUpToSeq);
2369
2821
  if (pipelineStopped)
2370
2822
  break;
2371
2823
  this.ensureDeliveryCurrent(ns, generation);
@@ -2442,14 +2894,7 @@ export class V2E2EECoordinator {
2442
2894
  result = pipelinePage.result;
2443
2895
  }
2444
2896
  else {
2445
- result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2446
- group_id: wireGroupId,
2447
- after_seq: nextAfterSeq,
2448
- limit,
2449
- ...cursorParams,
2450
- ...(opts?.force ? { force: true } : {}),
2451
- ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
2452
- }, groupAid), pullGateKey);
2897
+ result = await fetchPage(nextAfterSeq, ackUpToSeq);
2453
2898
  }
2454
2899
  if (ownsCursor)
2455
2900
  this.ensureDeliveryCurrent(ns, generation);
@@ -2486,6 +2931,7 @@ export class V2E2EECoordinator {
2486
2931
  .map((msg) => Number(msg.seq ?? 0))
2487
2932
  .filter((seq) => Number.isFinite(seq) && seq > 0);
2488
2933
  const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
2934
+ const namespaceGeneration = client._delivery.captureSeqNamespaceGeneration?.(ns);
2489
2935
  let pageTrackerSnapshot = null;
2490
2936
  let trackerMutated = false;
2491
2937
  let pageMaxSeq = nextAfterSeq;
@@ -2503,30 +2949,66 @@ export class V2E2EECoordinator {
2503
2949
  const deferredKeyFetches = new Map();
2504
2950
  let blockedSeq = 0;
2505
2951
  const pageDecryptedBefore = decrypted.length;
2506
- for (const msg of messages) {
2507
- const seq = Number(msg.seq ?? 0);
2508
- if (!Number.isFinite(seq) || seq <= 0)
2509
- continue;
2510
- const version = String(msg.version ?? 'v2');
2511
- if (version === 'v1') {
2512
- const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
2513
- const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
2514
- const payloadObj = isJsonObject(payload) ? payload : null;
2515
- const payloadType = payloadObj ? String(payloadObj.type ?? '').trim() : '';
2516
- // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
2517
- if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
2518
- && client._delivery.recallEventFromGroupMessage(msg)) {
2519
- await client._delivery.publishGroupRecallTombstone(gid, seq, {
2520
- ...msg,
2521
- group_id: gid,
2522
- group_aid: eventGroupAid,
2523
- }, 'pull');
2524
- client._markPublishedSeq(ns, seq);
2525
- client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
2952
+ const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
2953
+ const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
2954
+ const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
2955
+ const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq'));
2956
+ const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
2957
+ const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
2958
+ let commitTarget = pageContigBefore;
2959
+ await this.withApplicationPage(ns, 'pull', async () => {
2960
+ for (const msg of messages) {
2961
+ const seq = Number(msg.seq ?? 0);
2962
+ if (!Number.isFinite(seq) || seq <= 0)
2526
2963
  continue;
2527
- }
2528
- if (payloadObj) {
2529
- if (!['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
2964
+ const version = String(msg.version ?? 'v2');
2965
+ if (version === 'v1') {
2966
+ const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
2967
+ const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
2968
+ const payloadObj = isJsonObject(payload) ? payload : null;
2969
+ const payloadType = payloadObj ? String(payloadObj.type ?? '').trim() : '';
2970
+ // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
2971
+ if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
2972
+ && client._delivery.recallEventFromGroupMessage(msg)) {
2973
+ await client._delivery.publishGroupRecallTombstone(gid, seq, {
2974
+ ...msg,
2975
+ group_id: gid,
2976
+ group_aid: eventGroupAid,
2977
+ }, 'pull');
2978
+ client._markPublishedSeq(ns, seq);
2979
+ client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
2980
+ continue;
2981
+ }
2982
+ if (payloadObj) {
2983
+ if (!['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
2984
+ let v1Msg = {
2985
+ message_id: String(msg.message_id ?? ''),
2986
+ from: String(msg.from_aid ?? ''),
2987
+ group_id: gid,
2988
+ group_aid: eventGroupAid,
2989
+ seq: msg.seq,
2990
+ type: String(msg.type ?? ''),
2991
+ timestamp: msg.t_server,
2992
+ payload,
2993
+ encrypted: false,
2994
+ need_render: msg.need_render !== false,
2995
+ };
2996
+ if (msg.sender_device_id !== undefined)
2997
+ v1Msg.sender_device_id = msg.sender_device_id;
2998
+ v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
2999
+ attachGatewayProximity(v1Msg, msg);
3000
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
3001
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
3002
+ }
3003
+ else {
3004
+ client._markPublishedSeq(ns, seq);
3005
+ }
3006
+ decrypted.push(v1Msg);
3007
+ client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
3008
+ continue;
3009
+ }
3010
+ }
3011
+ else if (payload !== undefined && payload !== null) {
2530
3012
  let v1Msg = {
2531
3013
  message_id: String(msg.message_id ?? ''),
2532
3014
  from: String(msg.from_aid ?? ''),
@@ -2537,114 +3019,81 @@ export class V2E2EECoordinator {
2537
3019
  timestamp: msg.t_server,
2538
3020
  payload,
2539
3021
  encrypted: false,
3022
+ need_render: msg.need_render !== false,
2540
3023
  };
2541
3024
  if (msg.sender_device_id !== undefined)
2542
3025
  v1Msg.sender_device_id = msg.sender_device_id;
2543
3026
  v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
2544
3027
  attachGatewayProximity(v1Msg, msg);
2545
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2546
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2547
- }
2548
- else {
2549
- client._markPublishedSeq(ns, seq);
2550
- }
3028
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2551
3029
  decrypted.push(v1Msg);
2552
3030
  client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
2553
3031
  continue;
2554
3032
  }
2555
- }
2556
- else if (payload !== undefined && payload !== null) {
2557
- let v1Msg = {
2558
- message_id: String(msg.message_id ?? ''),
2559
- from: String(msg.from_aid ?? ''),
2560
- group_id: gid,
2561
- group_aid: eventGroupAid,
2562
- seq: msg.seq,
2563
- type: String(msg.type ?? ''),
2564
- timestamp: msg.t_server,
2565
- payload,
2566
- encrypted: false,
2567
- };
2568
- if (msg.sender_device_id !== undefined)
2569
- v1Msg.sender_device_id = msg.sender_device_id;
2570
- v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
2571
- attachGatewayProximity(v1Msg, msg);
2572
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2573
- decrypted.push(v1Msg);
2574
- client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
2575
- continue;
2576
- }
2577
- if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
2578
- const deferStatus = {};
2579
- const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
2580
- if (deferStatus.deferred) {
2581
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2582
- if (deferStatus.fromAid) {
2583
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2584
- deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
3033
+ if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
3034
+ const deferStatus = {};
3035
+ const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
3036
+ if (deferStatus.deferred) {
3037
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
3038
+ if (deferStatus.fromAid) {
3039
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
3040
+ deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
3041
+ }
2585
3042
  }
2586
- }
2587
- if (plaintext) {
2588
- plaintext.group_id = gid;
2589
- plaintext.group_aid = eventGroupAid;
2590
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2591
- await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2592
- }
2593
- else {
2594
- client._markPublishedSeq(ns, seq);
3043
+ if (plaintext) {
3044
+ plaintext.group_id = gid;
3045
+ plaintext.group_aid = eventGroupAid;
3046
+ plaintext.need_render = typeof msg.need_render === 'boolean' ? msg.need_render : plaintext.need_render !== false;
3047
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
3048
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
3049
+ }
3050
+ else {
3051
+ client._markPublishedSeq(ns, seq);
3052
+ }
3053
+ decrypted.push(plaintext);
2595
3054
  }
2596
- decrypted.push(plaintext);
2597
3055
  }
3056
+ else {
3057
+ client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
3058
+ }
3059
+ continue;
3060
+ }
3061
+ if (version !== 'v2') {
3062
+ client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
3063
+ continue;
3064
+ }
3065
+ const deferStatus = {};
3066
+ let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
3067
+ if (deferStatus.deferred) {
3068
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
3069
+ }
3070
+ if (deferStatus.deferred && deferStatus.fromAid) {
3071
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
3072
+ deferredKeyFetches.set(key, {
3073
+ fromAid: deferStatus.fromAid,
3074
+ senderDeviceId: deferStatus.senderDeviceId ?? '',
3075
+ groupId: deferStatus.groupId ?? '',
3076
+ });
3077
+ }
3078
+ if (plaintext === null) {
3079
+ client._clientLog.debug(`group.v2.pull decrypt returned null: group=${gid}, seq=${seq}`);
3080
+ continue;
3081
+ }
3082
+ plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
3083
+ plaintext.group_id = gid;
3084
+ plaintext.group_aid = eventGroupAid;
3085
+ plaintext.need_render = typeof msg.need_render === 'boolean' ? msg.need_render : plaintext.need_render !== false;
3086
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
3087
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2598
3088
  }
2599
3089
  else {
2600
- client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
3090
+ client._markPublishedSeq(ns, seq);
2601
3091
  }
2602
- continue;
3092
+ decrypted.push(plaintext);
3093
+ client._logMessageDebug('decrypt-ok', 'group.v2.pull', 'group.message_created', plaintext);
2603
3094
  }
2604
- if (version !== 'v2') {
2605
- client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
2606
- continue;
2607
- }
2608
- const deferStatus = {};
2609
- let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
2610
- if (deferStatus.deferred) {
2611
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2612
- }
2613
- if (deferStatus.deferred && deferStatus.fromAid) {
2614
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2615
- deferredKeyFetches.set(key, {
2616
- fromAid: deferStatus.fromAid,
2617
- senderDeviceId: deferStatus.senderDeviceId ?? '',
2618
- groupId: deferStatus.groupId ?? '',
2619
- });
2620
- }
2621
- if (plaintext === null) {
2622
- client._clientLog.debug(`group.v2.pull decrypt returned null: group=${gid}, seq=${seq}`);
2623
- continue;
2624
- }
2625
- plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
2626
- plaintext.group_id = gid;
2627
- plaintext.group_aid = eventGroupAid;
2628
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2629
- await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2630
- }
2631
- else {
2632
- client._markPublishedSeq(ns, seq);
2633
- }
2634
- decrypted.push(plaintext);
2635
- client._logMessageDebug('decrypt-ok', 'group.v2.pull', 'group.message_created', plaintext);
2636
- }
2637
- const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
2638
- const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
2639
- const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
2640
- const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq'));
2641
- const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
2642
- const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
2643
- const commitTarget = Math.max(pageContigBefore, effectiveFloor, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, ownsCursor ? cursorCurrentSeq : 0, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
2644
- let ackSeq = pageContigBefore;
2645
- let contigAdvanced = false;
2646
- if (ownsCursor) {
2647
- try {
3095
+ commitTarget = Math.max(pageContigBefore, effectiveFloor, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, ownsCursor ? cursorCurrentSeq : 0, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
3096
+ if (ownsCursor) {
2648
3097
  this.ensureDeliveryCurrent(ns, generation);
2649
3098
  const window = client._seqTracker.getSyncState?.(ns);
2650
3099
  const deliveryTarget = window && commitTarget >= window.tail - 1
@@ -2653,6 +3102,16 @@ export class V2E2EECoordinator {
2653
3102
  await client._drainOrderedMessages(ns, undefined, false, false, 'pull', deliveryTarget);
2654
3103
  }
2655
3104
  this.ensureDeliveryCurrent(ns, generation);
3105
+ }
3106
+ });
3107
+ let ackSeq = pageContigBefore;
3108
+ let contigAdvanced = false;
3109
+ if (ownsCursor) {
3110
+ try {
3111
+ this.ensureDeliveryCurrent(ns, generation);
3112
+ if (client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) === false)
3113
+ throw new Error('pull invalidated');
3114
+ client._delivery.rememberCommittedSeqState?.(ns);
2656
3115
  pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
2657
3116
  if (commitTarget > pageContigBefore) {
2658
3117
  trackerMutated = true;
@@ -2672,12 +3131,16 @@ export class V2E2EECoordinator {
2672
3131
  if (contigAdvanced) {
2673
3132
  await client._commitSeqTrackerState(ns);
2674
3133
  this.ensureDeliveryCurrent(ns, generation);
3134
+ if (client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) === false)
3135
+ throw new Error('pull invalidated');
2675
3136
  if (pageCount === 1 && deferredServerCursor > 0)
2676
3137
  this.clearForwardCursor(ns, ackSeq);
2677
3138
  }
3139
+ committedAckSeq = ackSeq;
2678
3140
  }
2679
3141
  catch (exc) {
2680
3142
  if (trackerMutated && Number(client._v2RuntimeGeneration ?? 0) === generation
3143
+ && client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) !== false
2681
3144
  && client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false
2682
3145
  && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
2683
3146
  (client._seqTracker._restoreNamespaceSnapshotPreservingPush ?? client._seqTracker.restoreNamespaceSnapshot)
@@ -2749,14 +3212,6 @@ export class V2E2EECoordinator {
2749
3212
  if (shouldContinueForPipeline && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
2750
3213
  startPipeline(nextAfter);
2751
3214
  }
2752
- if (!pipelineRunning && pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
2753
- const nextKey = pullGateKeyForClient(client, 'group.v2.pull', {
2754
- group_id: gid, after_seq: nextAfter, force: opts?.force === true, limit,
2755
- _group_cursor_params: cursorParams,
2756
- }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
2757
- await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
2758
- pullGateKey = nextKey;
2759
- }
2760
3215
  const tailDelayMs = pipelineRunning ? 0 : v2PullTailDelayMs(rawCount, limit);
2761
3216
  if (tailDelayMs > 0)
2762
3217
  await v2Sleep(tailDelayMs);
@@ -2773,7 +3228,7 @@ export class V2E2EECoordinator {
2773
3228
  this.ensureDeliveryCurrent(ns, generation);
2774
3229
  client._clientLog.debug(`group.v2.pull flushing pending auto-ack: group=${gid}, ns=${ns}, ack_seq=${pendingAckSeq}`);
2775
3230
  const ackToFlush = pendingAckSeq;
2776
- await this.confirmForwardGroupAck(gid, ackToFlush, groupAid);
3231
+ await this.confirmForwardGroupAck(gid, ackToFlush, groupAid, background);
2777
3232
  if (pendingAckSeq <= ackToFlush)
2778
3233
  pendingAckSeq = 0;
2779
3234
  }
@@ -2794,11 +3249,11 @@ export class V2E2EECoordinator {
2794
3249
  throw error;
2795
3250
  }
2796
3251
  }
2797
- async confirmForwardGroupAck(groupId, upToSeq, groupAid) {
3252
+ async confirmForwardGroupAck(groupId, upToSeq, groupAid, background = false) {
2798
3253
  const ns = `group:${String(groupId ?? '').trim()}`;
2799
3254
  const generation = Number(this.client._v2RuntimeGeneration ?? 0);
2800
3255
  try {
2801
- const result = await this.ackGroupV2ForForward(groupId, upToSeq, groupAid);
3256
+ const result = await this.ackGroupV2ForForward(groupId, upToSeq, groupAid, background);
2802
3257
  this.ensureDeliveryCurrent(ns, generation);
2803
3258
  const actualAckSeq = this.client._delivery.resolveForwardAckSeq(result, upToSeq);
2804
3259
  if (actualAckSeq < upToSeq) {
@@ -2816,10 +3271,10 @@ export class V2E2EECoordinator {
2816
3271
  async ackGroupV2(groupId, upToSeq, groupAid) {
2817
3272
  return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, false);
2818
3273
  }
2819
- async ackGroupV2ForForward(groupId, upToSeq, groupAid) {
2820
- return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, true);
3274
+ async ackGroupV2ForForward(groupId, upToSeq, groupAid, background = false) {
3275
+ return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, true, background);
2821
3276
  }
2822
- async ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, requireServerWatermark) {
3277
+ async ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, requireServerWatermark, background) {
2823
3278
  const client = this.client;
2824
3279
  const gid = String(groupId ?? '').trim();
2825
3280
  if (!gid)
@@ -2837,7 +3292,9 @@ export class V2E2EECoordinator {
2837
3292
  return { acked: 0 };
2838
3293
  const inlineAckGeneration = client._delivery.captureGroupInlineAckGeneration();
2839
3294
  client._clientLog.debug(`group.v2.ack send: group=${gid}, ns=${ns}, up_to_seq=${seq}`);
2840
- const result = await client._callRawV2Rpc('group.v2.ack', withExplicitGroupAid({ group_id: gid, up_to_seq: seq }, explicitGroupAid));
3295
+ const result = await client._callRawV2Rpc('group.v2.ack', withExplicitGroupAid({ group_id: gid, up_to_seq: seq,
3296
+ ...(background === undefined ? {} : { _rpc_background: background, _rpc_foreground: !background }),
3297
+ }, explicitGroupAid));
2841
3298
  const actualAckSeq = requireServerWatermark
2842
3299
  ? client._delivery.resolveForwardAckSeq(result, seq)
2843
3300
  : client._delivery.resolveActualAckSeq(result, seq);