@agentunion/fastaun 0.5.19 → 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 (129) hide show
  1. package/CHANGELOG.md +176 -78
  2. package/_packed_docs/CHANGELOG.md +176 -78
  3. package/_packed_docs/INDEX.md +288 -269
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +101 -95
  5. package/_packed_docs/agent.md/SCHEMA.md +7 -7
  6. 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
  7. 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
  8. 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
  9. 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
  10. package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +11 -11
  11. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +372 -331
  12. package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
  13. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +676 -676
  14. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +8 -12
  15. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +445 -0
  16. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.0.md +161 -0
  17. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +666 -0
  18. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
  19. package/_packed_docs/protocol/agent.md/SCHEMA.md +1 -1
  20. 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
  21. 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
  22. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
  23. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
  24. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
  25. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +70 -63
  26. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +43 -40
  27. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +179 -19
  28. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +50 -49
  29. package/_packed_docs/sdk/09-group-rpc-manual.md +261 -254
  30. package/_packed_docs/sdk/09-message-rpc-manual.md +253 -160
  31. package/_packed_docs/sdk/09-storage-rpc-manual.md +18 -26
  32. 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
  33. 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
  34. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +105 -92
  35. package/_packed_docs/sdk/INDEX.md +197 -167
  36. package/_packed_docs/sdk/Ownership-SDK/345/256/236/347/216/260/346/214/207/345/215/227.md +169 -0
  37. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/227.md +349 -0
  38. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +930 -0
  39. package/_packed_docs/sdk/Ownership-SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +270 -0
  40. 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 +363 -0
  41. 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 +553 -0
  42. 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 -0
  43. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +480 -0
  44. package/_packed_docs/sdk/Ownership/347/256/241/347/220/206/344/270/223/351/242/230.md +559 -0
  45. package/_packed_docs/sdk/ProtectedHeaders/347/224/250/346/263/225/344/270/223/351/242/230.md +299 -0
  46. 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 +173 -0
  47. 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 +211 -0
  48. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/345/256/236/347/216/260/347/247/273/346/244/215/346/214/207/345/215/227.md +331 -0
  49. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +207 -0
  50. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/344/273/243/347/240/201/345/256/241/346/237/245/346/212/245/345/221/212.md +252 -0
  51. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/345/256/236/346/226/275/350/256/241/345/210/222.md +156 -0
  52. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/346/226/271/346/241/210.md +273 -0
  53. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/350/257/246/347/273/206/350/256/276/350/256/241.md +211 -0
  54. package/dist/agent-md-schema.d.ts +13 -1
  55. package/dist/agent-md-schema.js +63 -33
  56. package/dist/agent-md-schema.js.map +1 -1
  57. package/dist/agent-md.d.ts +30 -3
  58. package/dist/agent-md.js +293 -152
  59. package/dist/agent-md.js.map +1 -1
  60. package/dist/aid-store.d.ts +42 -0
  61. package/dist/aid-store.js +224 -44
  62. package/dist/aid-store.js.map +1 -1
  63. package/dist/aid.js +18 -13
  64. package/dist/aid.js.map +1 -1
  65. package/dist/auth.js +78 -2
  66. package/dist/auth.js.map +1 -1
  67. package/dist/cert-utils.d.ts +44 -1
  68. package/dist/cert-utils.js +157 -56
  69. package/dist/cert-utils.js.map +1 -1
  70. package/dist/client/delivery.d.ts +68 -1
  71. package/dist/client/delivery.js +1196 -135
  72. package/dist/client/delivery.js.map +1 -1
  73. package/dist/client/rpc-pipeline.d.ts +5 -1
  74. package/dist/client/rpc-pipeline.js +240 -111
  75. package/dist/client/rpc-pipeline.js.map +1 -1
  76. package/dist/client/v2-e2ee.d.ts +12 -2
  77. package/dist/client/v2-e2ee.js +842 -379
  78. package/dist/client/v2-e2ee.js.map +1 -1
  79. package/dist/client.d.ts +26 -2
  80. package/dist/client.js +420 -75
  81. package/dist/client.js.map +1 -1
  82. package/dist/config.d.ts +4 -0
  83. package/dist/config.js +1 -0
  84. package/dist/config.js.map +1 -1
  85. package/dist/events.d.ts +23 -4
  86. package/dist/events.js +171 -15
  87. package/dist/events.js.map +1 -1
  88. package/dist/facades.d.ts +43 -1
  89. package/dist/facades.js +368 -12
  90. package/dist/facades.js.map +1 -1
  91. package/dist/index.d.ts +7 -4
  92. package/dist/index.js +1 -1
  93. package/dist/index.js.map +1 -1
  94. package/dist/keystore/aid-db.d.ts +7 -2
  95. package/dist/keystore/aid-db.js +194 -43
  96. package/dist/keystore/aid-db.js.map +1 -1
  97. package/dist/keystore/index.d.ts +15 -1
  98. package/dist/keystore/local-token-store.d.ts +10 -2
  99. package/dist/keystore/local-token-store.js +81 -16
  100. package/dist/keystore/local-token-store.js.map +1 -1
  101. package/dist/org-protocol.d.ts +45 -0
  102. package/dist/org-protocol.js +829 -22
  103. package/dist/org-protocol.js.map +1 -1
  104. package/dist/ownership-v21.d.ts +40 -9
  105. package/dist/ownership-v21.js +174 -79
  106. package/dist/ownership-v21.js.map +1 -1
  107. package/dist/ownership-workflow.d.ts +22 -0
  108. package/dist/ownership-workflow.js +861 -66
  109. package/dist/ownership-workflow.js.map +1 -1
  110. package/dist/secret-store/file-store.js +84 -0
  111. package/dist/secret-store/file-store.js.map +1 -1
  112. package/dist/seq-tracker.d.ts +1 -0
  113. package/dist/seq-tracker.js +2 -2
  114. package/dist/seq-tracker.js.map +1 -1
  115. package/dist/tools/cross-sdk-agent.d.ts +67 -3
  116. package/dist/tools/cross-sdk-agent.js +575 -42
  117. package/dist/tools/cross-sdk-agent.js.map +1 -1
  118. package/dist/transport.d.ts +4 -0
  119. package/dist/transport.js +108 -44
  120. package/dist/transport.js.map +1 -1
  121. package/dist/types.d.ts +1 -0
  122. package/dist/v2/e2ee/encrypt-group.js +1 -0
  123. package/dist/v2/e2ee/encrypt-group.js.map +1 -1
  124. package/dist/v2/e2ee/encrypt-p2p.js +1 -0
  125. package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
  126. package/dist/v2/e2ee/types.d.ts +1 -0
  127. package/dist/version.d.ts +1 -1
  128. package/dist/version.js +1 -1
  129. 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';
@@ -113,13 +114,13 @@ function deduplicateForwardPageMessages(value, afterSeq) {
113
114
  function contiguousPullSeq(afterSeq, seqs, proof) {
114
115
  const pulledMax = seqs.length > 0 ? Math.max(...seqs) : 0;
115
116
  const serverProof = safeNonNegativeSeq(proof);
116
- return Math.max(afterSeq, pulledMax, serverProof ?? 0);
117
+ return Math.max(afterSeq, pulledMax, pulledMax > 0 ? 0 : serverProof ?? 0);
117
118
  }
118
119
  function shouldContinueForwardPage({ rawCount, nextAfterSeq, nextAfter, limit, hasMoreField, hasMore, serverHead, observedUpper, pendingAckSeq, }) {
119
120
  // 无新 seq 或游标未前进时必须停止,避免错误元数据造成循环。
120
121
  if (rawCount <= 0 || nextAfter <= nextAfterSeq)
121
122
  return false;
122
- if (hasMore || serverHead > nextAfter || observedUpper > nextAfter || pendingAckSeq > 0)
123
+ if (hasMore || serverHead > nextAfter || observedUpper > nextAfter)
123
124
  return true;
124
125
  // 仅旧服务缺失 has_more 时,以满页保留兼容续拉。
125
126
  return !hasMoreField && limit > 0 && rawCount >= limit;
@@ -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 {
@@ -917,10 +944,14 @@ export class V2E2EECoordinator {
917
944
  });
918
945
  }
919
946
  }
947
+ const targetDeviceId = String(opts.deviceId ?? '').trim();
948
+ if (useCache && targetDeviceId && !peerDevices.some((dev) => String(dev.device_id ?? '').trim() === targetDeviceId)) {
949
+ this.deleteBootstrapCacheEntry(to);
950
+ return this.buildV2P2PEnvelope({ ...opts, useCache: false });
951
+ }
920
952
  if (peerDevices.length === 0) {
921
953
  throw new E2EEError(`V2 bootstrap: no devices found for ${to}`);
922
954
  }
923
- const targetDeviceId = String(opts.deviceId ?? '').trim();
924
955
  if (targetDeviceId && !peerDevices.some((dev) => String(dev.device_id ?? '').trim() === targetDeviceId)) {
925
956
  throw new E2EEError(`V2 bootstrap: target device ${targetDeviceId} not found for ${to}`);
926
957
  }
@@ -1007,6 +1038,10 @@ export class V2E2EECoordinator {
1007
1038
  targetSet = this.cloneP2PTargetSet(targetSet);
1008
1039
  }
1009
1040
  if (targetSet.targets.length === 0) {
1041
+ if (useCache && targetDeviceId) {
1042
+ this.deleteBootstrapCacheEntry(to);
1043
+ return this.buildV2P2PEnvelope({ ...opts, useCache: false });
1044
+ }
1010
1045
  throw new E2EEError(`V2 bootstrap: no usable devices found for ${to}`);
1011
1046
  }
1012
1047
  if (targetDeviceId) {
@@ -1020,6 +1055,10 @@ export class V2E2EECoordinator {
1020
1055
  return isPeerTarget || target.role === 'self_sync';
1021
1056
  });
1022
1057
  if (!peerTargetFound) {
1058
+ if (useCache) {
1059
+ this.deleteBootstrapCacheEntry(to);
1060
+ return this.buildV2P2PEnvelope({ ...opts, useCache: false });
1061
+ }
1023
1062
  throw new E2EEError(`V2 bootstrap: target device ${targetDeviceId} not found for ${to}`);
1024
1063
  }
1025
1064
  }
@@ -1028,6 +1067,7 @@ export class V2E2EECoordinator {
1028
1067
  timestamp: opts.timestamp,
1029
1068
  protectedHeaders: opts.protectedHeaders,
1030
1069
  context: opts.context,
1070
+ needRender: opts.needRender,
1031
1071
  });
1032
1072
  client._logMessageDebug('send-envelope', 'message.send.v2', 'message.send', {
1033
1073
  message_id: envelope.message_id,
@@ -1072,6 +1112,7 @@ export class V2E2EECoordinator {
1072
1112
  deviceId: targetDeviceId || undefined,
1073
1113
  protectedHeaders: opts?.protectedHeaders,
1074
1114
  context: opts?.context,
1115
+ needRender: opts?.needRender,
1075
1116
  useCache,
1076
1117
  });
1077
1118
  const result = await client.call('message.send', {
@@ -1081,6 +1122,7 @@ export class V2E2EECoordinator {
1081
1122
  ...(targetDeviceId ? { device_id: targetDeviceId } : {}),
1082
1123
  ...(opts?.ttl !== undefined ? { ttl: opts.ttl } : {}),
1083
1124
  encrypt: false,
1125
+ need_render: opts?.needRender ?? true,
1084
1126
  _skip_send_result_envelope: true,
1085
1127
  });
1086
1128
  client._clientLog.debug(`message.v2.send ok: to=${toAid}, use_cache=${useCache}, seq=${String((isJsonObject(result) ? result.seq : '') ?? '')}`);
@@ -1094,6 +1136,7 @@ export class V2E2EECoordinator {
1094
1136
  message_id: messageId,
1095
1137
  timestamp: opts?.timestamp,
1096
1138
  ttl: opts?.ttl,
1139
+ need_render: opts?.needRender ?? true,
1097
1140
  protected_headers: opts?.protectedHeaders,
1098
1141
  context: opts?.context,
1099
1142
  }, result, true);
@@ -1110,6 +1153,7 @@ export class V2E2EECoordinator {
1110
1153
  message_id: messageId,
1111
1154
  timestamp: opts?.timestamp,
1112
1155
  ttl: opts?.ttl,
1156
+ need_render: opts?.needRender ?? true,
1113
1157
  protected_headers: opts?.protectedHeaders,
1114
1158
  context: opts?.context,
1115
1159
  }, result, true);
@@ -1125,12 +1169,13 @@ export class V2E2EECoordinator {
1125
1169
  history_error: { code: 'decrypt_failed', message: reason },
1126
1170
  };
1127
1171
  }
1128
- ensureDeliveryCurrent(ns, generation) {
1172
+ ensureDeliveryCurrent(ns, generation, inlineGeneration) {
1129
1173
  if (Number(this.client._v2RuntimeGeneration ?? 0) !== generation
1174
+ || (inlineGeneration !== undefined && this.client._delivery.captureInlineAckGeneration() !== inlineGeneration)
1130
1175
  || this.client._rpcPipeline?.isPullNamespaceCurrent?.(ns) === false)
1131
1176
  throw new StateError('pull invalidated');
1132
1177
  }
1133
- async decodeP2PWindowMessage(msg, publish, history, source, orderedPublish = false) {
1178
+ async decodeP2PWindowMessage(msg, publish, history, source, orderedPublish = false, deferredEffects) {
1134
1179
  const client = this.client;
1135
1180
  const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
1136
1181
  let plaintext = null;
@@ -1161,11 +1206,11 @@ export class V2E2EECoordinator {
1161
1206
  else if (isJsonObject(payload)
1162
1207
  && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1163
1208
  msg = { ...msg, envelope_json: JSON.stringify(payload) };
1164
- 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] : []));
1165
1210
  }
1166
1211
  }
1167
1212
  else {
1168
- 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] : []));
1169
1214
  }
1170
1215
  if (!plaintext) {
1171
1216
  if (history)
@@ -1248,6 +1293,7 @@ export class V2E2EECoordinator {
1248
1293
  client._clientLog.warn(`Group Tail 单条消息解密失败,连续性证明仍保留: group=${groupId}, seq=${seq}`);
1249
1294
  return null;
1250
1295
  }
1296
+ plaintext.need_render = typeof msg.need_render === 'boolean' ? msg.need_render : plaintext.need_render !== false;
1251
1297
  if (publish && !history && client._delivery.isSelfSentGroupMessage?.(msg)) {
1252
1298
  return plaintext;
1253
1299
  }
@@ -1256,6 +1302,363 @@ export class V2E2EECoordinator {
1256
1302
  }
1257
1303
  return plaintext;
1258
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
+ }
1259
1662
  async pullV2TailInternal(params, deferPublish = false) {
1260
1663
  const client = this.client;
1261
1664
  const generation = Number(client._v2RuntimeGeneration ?? 0);
@@ -1274,12 +1677,14 @@ export class V2E2EECoordinator {
1274
1677
  const result = await client._callRawV2Rpc('message.v2.pull', { window_mode: 'tail', after_seq: afterSeq, limit, _rpc_foreground: true }, pullGateKey);
1275
1678
  const page = validateTailPage(result, afterSeq, limit);
1276
1679
  const decoded = [];
1277
- for (const msg of page.messages) {
1278
- this.ensureDeliveryCurrent(ns, generation);
1279
- const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail', deferPublish);
1280
- if (plaintext)
1281
- decoded.push(plaintext);
1282
- }
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
+ });
1283
1688
  this.ensureDeliveryCurrent(ns, generation);
1284
1689
  if (ns) {
1285
1690
  client._seqTracker.commitTailWindow(ns, {
@@ -1361,10 +1766,9 @@ export class V2E2EECoordinator {
1361
1766
  const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
1362
1767
  let pipelineRunning = false;
1363
1768
  let pipelineError = null;
1364
- const pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
1769
+ let pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
1365
1770
  let pipelineProducer = null;
1366
1771
  let pipelineStopped = false;
1367
- const pipelineResume = { current: null };
1368
1772
  const startPipeline = (startAfterSeq, startVolatileCursor) => {
1369
1773
  if (pipelineRunning || pageCount >= maxPages || limit < V2_PULL_MAX_PAGE_LIMIT)
1370
1774
  return;
@@ -1375,30 +1779,31 @@ export class V2E2EECoordinator {
1375
1779
  for (let produced = pageCount; produced < maxPages; produced += 1) {
1376
1780
  if (pipelineStopped)
1377
1781
  break;
1782
+ this.ensureDeliveryCurrent(ns, generation);
1378
1783
  const requestedVolatileCursor = producerVolatileCursor;
1784
+ const ackUpToSeq = pendingAckSeq;
1785
+ const inlineAckGeneration = ackUpToSeq > 0 ? client._delivery.captureP2PInlineAckGeneration() : null;
1379
1786
  const result = await client._callRawV2Rpc('message.v2.pull', {
1380
1787
  after_seq: producerAfterSeq,
1381
1788
  limit,
1789
+ ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
1382
1790
  ...(producerVolatileCursor ? { after_volatile_cursor: producerVolatileCursor } : {}),
1383
1791
  ...(opts?.force ? { force: true } : {}),
1384
1792
  }, pullGateKey);
1385
1793
  if (pipelineStopped)
1386
1794
  break;
1795
+ this.ensureDeliveryCurrent(ns, generation);
1387
1796
  const rawPageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1388
- const completed = new Promise((resolve) => { pipelineResume.current = resolve; });
1389
1797
  const enqueued = await pipelineQueue.put({
1390
1798
  result,
1391
1799
  afterSeq: producerAfterSeq,
1392
- ackUpToSeq: 0,
1393
- inlineAckGeneration: null,
1800
+ ackUpToSeq,
1801
+ inlineAckGeneration,
1394
1802
  requestedVolatileCursor,
1395
1803
  weight: rawPageMessages.length,
1396
1804
  }, rawPageMessages.length);
1397
1805
  if (!enqueued || pipelineStopped)
1398
1806
  break;
1399
- await completed;
1400
- if (pipelineStopped)
1401
- break;
1402
1807
  const volatileMessages = client._delivery.volatileMessages?.(rawPageMessages) ?? [];
1403
1808
  if (volatileMessages.length > 0)
1404
1809
  break;
@@ -1406,7 +1811,7 @@ export class V2E2EECoordinator {
1406
1811
  const seqs = messages
1407
1812
  .map((msg) => Number(msg.seq ?? 0))
1408
1813
  .filter((seq) => Number.isFinite(seq) && seq > 0);
1409
- const nextAfter = Math.max(contiguousPullSeq(producerAfterSeq, seqs, result.contiguous_seq), producerAfterSeq);
1814
+ const nextAfter = Math.max(producerAfterSeq, ...seqs);
1410
1815
  const cursor = isJsonObject(result.cursor)
1411
1816
  ? result.cursor : null;
1412
1817
  const shouldContinue = shouldContinueForwardPage({
@@ -1433,8 +1838,6 @@ export class V2E2EECoordinator {
1433
1838
  };
1434
1839
  const stopPipeline = async () => {
1435
1840
  pipelineStopped = true;
1436
- pipelineResume.current?.();
1437
- pipelineResume.current = null;
1438
1841
  pipelineQueue.close();
1439
1842
  if (pipelineProducer) {
1440
1843
  const producer = pipelineProducer;
@@ -1458,7 +1861,7 @@ export class V2E2EECoordinator {
1458
1861
  pageCount += 1;
1459
1862
  const requestedVolatileCursor = pipelinePage?.requestedVolatileCursor ?? volatileCursor;
1460
1863
  const ackUpToSeq = pipelinePage?.ackUpToSeq ?? pendingAckSeq;
1461
- const inlineAckGeneration = pipelinePage?.inlineAckGeneration ?? (ackUpToSeq > 0
1864
+ const inlineAckGeneration = pipelinePage ? pipelinePage.inlineAckGeneration : (ackUpToSeq > 0
1462
1865
  ? client._delivery.captureP2PInlineAckGeneration()
1463
1866
  : null);
1464
1867
  client._clientLog.debug(`message.v2.pull page request: page=${pageCount}, after_seq=${nextAfterSeq}, limit=${limit}, ack_up_to_seq=${ackUpToSeq}, ns=${ns || '<none>'}`);
@@ -1487,28 +1890,30 @@ export class V2E2EECoordinator {
1487
1890
  continue;
1488
1891
  }
1489
1892
  }
1893
+ this.ensureDeliveryCurrent(ns, generation);
1490
1894
  if (ackUpToSeq > 0) {
1491
1895
  const actualAckSeq = client._delivery.resolveP2PPullAckSeq(result, ackUpToSeq);
1492
1896
  if (actualAckSeq >= ackUpToSeq) {
1493
1897
  if (inlineAckGeneration !== null) {
1494
1898
  client._delivery.onP2PV2PullAckResult(result, ackUpToSeq, inlineAckGeneration);
1495
1899
  }
1496
- pendingAckSeq = 0;
1900
+ if (pendingAckSeq <= ackUpToSeq)
1901
+ pendingAckSeq = 0;
1497
1902
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1498
1903
  this.clearForwardAck(ns, ackUpToSeq);
1499
1904
  }
1500
1905
  else {
1501
1906
  client._clientLog.warn(`message.v2.pull piggyback ACK below target: requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
1502
- await this.confirmForwardP2PAck(ackUpToSeq);
1503
- pendingAckSeq = 0;
1504
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1505
1907
  }
1506
1908
  }
1507
1909
  const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1508
1910
  const volatileMessages = client._delivery.volatileMessages?.(pageMessages) ?? [];
1509
- if (volatileMessages.length > 0) {
1510
- decrypted.push(...await this.publishV2VolatileMessages(volatileMessages, 'pull', true));
1511
- volatileCursor = String(client._delivery.volatilePullCursor?.() ?? '').trim();
1911
+ if (pipelinePage && volatileMessages.length > 0) {
1912
+ if (pipelineProducer)
1913
+ await pipelineProducer;
1914
+ pipelineProducer = null;
1915
+ pipelineRunning = false;
1916
+ pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
1512
1917
  }
1513
1918
  const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
1514
1919
  const cursor = isJsonObject(result.cursor) ? result.cursor : null;
@@ -1531,118 +1936,128 @@ export class V2E2EECoordinator {
1531
1936
  pageMaxSeq = Math.max(...seqs);
1532
1937
  }
1533
1938
  const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
1939
+ if (!pipelineRunning && volatileMessages.length === 0 && shouldContinueForwardPage({
1940
+ rawCount: messages.length, nextAfterSeq, nextAfter: pageMaxSeq, limit,
1941
+ hasMoreField, hasMore, serverHead,
1942
+ observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._pendingP2pPullUpper?.get?.(ns) ?? 0)),
1943
+ pendingAckSeq: 0,
1944
+ }))
1945
+ startPipeline(pageMaxSeq, volatileCursor);
1534
1946
  const deferredKeyFetches = new Map();
1535
1947
  let blockedSeq = 0;
1536
- const pageDecryptedBefore = decrypted.length;
1537
- for (const msg of messages) {
1538
- const seq = Number(msg.seq ?? 0);
1539
- if (!Number.isFinite(seq) || seq <= 0)
1540
- continue;
1541
- const version = String(msg.version ?? 'v2');
1542
- if (version === 'v1') {
1543
- const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
1544
- const legacyPayload = legacy.payload !== undefined ? legacy.payload : msg.payload;
1545
- const payloadType = isJsonObject(legacyPayload)
1546
- ? String(legacyPayload.type ?? '').trim()
1547
- : '';
1548
- if (legacyPayload !== undefined && legacyPayload !== null
1549
- && !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
1550
- const v1Msg = {
1551
- message_id: String(msg.message_id ?? ''),
1552
- from: String(msg.from_aid ?? ''),
1553
- to: String(legacy.to ?? client._aid ?? ''),
1554
- seq: msg.seq,
1555
- type: String(msg.type ?? ''),
1556
- timestamp: msg.t_server,
1557
- payload: legacyPayload,
1558
- encrypted: false,
1559
- };
1560
- const direction = String(msg.direction ?? '').trim();
1561
- if (direction)
1562
- v1Msg.direction = direction;
1563
- else if (String(msg.from_aid ?? '').trim() && String(msg.from_aid ?? '').trim() !== String(client._aid ?? '').trim()) {
1564
- 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>'}`);
1565
2000
  }
1566
- if (msg.sender_device_id !== undefined)
1567
- v1Msg.sender_device_id = msg.sender_device_id;
1568
- attachGatewayProximity(v1Msg, msg);
1569
- const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
1570
- if (ns) {
1571
- 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
+ }
1572
2019
  }
1573
2020
  else {
1574
- 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)`);
1575
2022
  }
1576
- decrypted.push(v1Msg);
1577
- client._clientLog.debug(`message.v2.pull plaintext V1 delivered: seq=${seq}, ns=${ns || '<none>'}`);
2023
+ continue;
1578
2024
  }
1579
- else if (isJsonObject(legacyPayload)
1580
- && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1581
- const deferStatus = {};
1582
- const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(legacyPayload) }, true, true, true, true, deferStatus, true, 'pull');
1583
- if (deferStatus.deferred) {
1584
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1585
- if (deferStatus.fromAid) {
1586
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1587
- deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
1588
- }
1589
- }
1590
- if (plaintext) {
1591
- if (ns)
1592
- await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1593
- else
1594
- await client._publishAppEvent('message.received', plaintext, 'pull');
1595
- decrypted.push(plaintext);
1596
- }
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);
1597
2052
  }
1598
2053
  else {
1599
- 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');
1600
2055
  }
1601
- continue;
1602
- }
1603
- if (version !== 'v2') {
1604
- client._clientLog.debug(`message.v2.pull skipping non-V2 row seq=${seq} version=${String(msg.version ?? '')}`);
1605
- continue;
1606
- }
1607
- const spkId = String(msg.spk_id ?? '');
1608
- if (spkId && client._v2Session && !client._v2Session.isCurrentSPK(spkId)) {
1609
- client._v2Session.trackOldSPKMaxSeq(spkId, seq);
1610
- }
1611
- const deferStatus = {};
1612
- const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
1613
- if (deferStatus.deferred) {
1614
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1615
- }
1616
- if (deferStatus.deferred && deferStatus.fromAid) {
1617
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1618
- deferredKeyFetches.set(key, {
1619
- fromAid: deferStatus.fromAid,
1620
- senderDeviceId: deferStatus.senderDeviceId ?? '',
1621
- groupId: deferStatus.groupId ?? '',
1622
- });
1623
- }
1624
- if (plaintext === null) {
1625
- client._clientLog.debug(`message.v2.pull decrypt returned null: seq=${seq}, ns=${ns || '<none>'}`);
1626
- continue;
2056
+ decrypted.push(plaintext);
2057
+ client._logMessageDebug('decrypt-ok', 'message.v2.pull', 'message.received', plaintext);
1627
2058
  }
1628
2059
  if (ns) {
1629
- await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1630
- }
1631
- else {
1632
- await client._publishAppEvent('message.received', plaintext, 'pull');
1633
- }
1634
- decrypted.push(plaintext);
1635
- client._logMessageDebug('decrypt-ok', 'message.v2.pull', 'message.received', plaintext);
1636
- }
1637
- const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
1638
- const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
1639
- const serverAckSeq = parsedServerAckSeq ?? 0;
1640
- const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
1641
- if (ns) {
1642
- const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, serverAckSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
1643
- let ackSeq = pageContigBefore;
1644
- let contigAdvanced = false;
1645
- try {
2060
+ commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, serverAckSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
1646
2061
  this.ensureDeliveryCurrent(ns, generation);
1647
2062
  const window = client._seqTracker.getSyncState?.(ns);
1648
2063
  const deliveryTarget = window && commitTarget >= window.tail - 1
@@ -1651,6 +2066,15 @@ export class V2E2EECoordinator {
1651
2066
  await client._drainOrderedMessages(ns, undefined, false, false, 'pull', deliveryTarget);
1652
2067
  }
1653
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);
1654
2078
  pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
1655
2079
  if (commitTarget > pageContigBefore) {
1656
2080
  trackerMutated = true;
@@ -1701,18 +2125,8 @@ export class V2E2EECoordinator {
1701
2125
  || (contigAdvanced && ackSeq > knownServerAckSeq));
1702
2126
  if (ackNeeded) {
1703
2127
  this.recordForwardAck(ns, ackSeq);
1704
- const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
1705
- const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq;
1706
- if (canContinuePage && !pipelineRunning) {
1707
- pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
1708
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1709
- client._clientLog.debug(`message.v2.pull queued piggyback auto-ack: ns=${ns}, ack_seq=${pendingAckSeq}, raw_count=${messages.length}`);
1710
- }
1711
- else {
1712
- client._clientLog.debug(`message.v2.pull sending terminal auto-ack: ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
1713
- await this.confirmForwardP2PAck(ackSeq);
1714
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1715
- }
2128
+ pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
2129
+ client._clientLog.debug(`message.v2.pull queued piggyback auto-ack: ns=${ns}, ack_seq=${pendingAckSeq}, raw_count=${messages.length}`);
1716
2130
  }
1717
2131
  }
1718
2132
  if (!ns) {
@@ -1729,7 +2143,7 @@ export class V2E2EECoordinator {
1729
2143
  currentSeq: client._seqTracker.getContiguousSeq(ns),
1730
2144
  targetSeq: serverHead,
1731
2145
  });
1732
- const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
2146
+ const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
1733
2147
  const rawCount = messages.length;
1734
2148
  if (blockedSeq > 0)
1735
2149
  break;
@@ -1761,11 +2175,7 @@ export class V2E2EECoordinator {
1761
2175
  && volatileCursor !== requestedVolatileCursor;
1762
2176
  if (!shouldContinuePersistent && !shouldContinueVolatile)
1763
2177
  break;
1764
- if (shouldContinueForPipeline && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
1765
- if (pendingAckSeq > 0) {
1766
- await this.confirmForwardP2PAck(pendingAckSeq);
1767
- pendingAckSeq = 0;
1768
- }
2178
+ if (shouldContinueForPipeline && volatileMessages.length === 0 && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
1769
2179
  startPipeline(nextAfter, volatileCursor);
1770
2180
  }
1771
2181
  if (!pipelineRunning && pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
@@ -1775,14 +2185,10 @@ export class V2E2EECoordinator {
1775
2185
  await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
1776
2186
  pullGateKey = nextKey;
1777
2187
  }
1778
- const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
2188
+ const tailDelayMs = pipelineRunning ? 0 : v2PullTailDelayMs(rawCount, limit);
1779
2189
  if (tailDelayMs > 0)
1780
2190
  await v2Sleep(tailDelayMs);
1781
2191
  nextAfterSeq = nextAfter;
1782
- if (pipelinePage) {
1783
- pipelineResume.current?.();
1784
- pipelineResume.current = null;
1785
- }
1786
2192
  }
1787
2193
  }
1788
2194
  finally {
@@ -1790,11 +2196,14 @@ export class V2E2EECoordinator {
1790
2196
  }
1791
2197
  if (pageCount >= maxPages) {
1792
2198
  client._clientLog.warn(`message.v2.pull reached max_pages=${maxPages} after_seq=${nextAfterSeq}`);
1793
- if (pendingAckSeq > 0) {
1794
- client._clientLog.debug(`message.v2.pull flushing pending auto-ack after max_pages: ns=${ns}, ack_seq=${pendingAckSeq}`);
1795
- await this.confirmForwardP2PAck(pendingAckSeq);
2199
+ }
2200
+ if (pendingAckSeq > 0) {
2201
+ this.ensureDeliveryCurrent(ns, generation);
2202
+ client._clientLog.debug(`message.v2.pull flushing pending auto-ack: ns=${ns}, ack_seq=${pendingAckSeq}`);
2203
+ const ackToFlush = pendingAckSeq;
2204
+ await this.confirmForwardP2PAck(ackToFlush);
2205
+ if (pendingAckSeq <= ackToFlush)
1796
2206
  pendingAckSeq = 0;
1797
- }
1798
2207
  }
1799
2208
  client._clientLog.debug(`message.v2.pull done: requested_after_seq=${afterSeq}, pages=${pageCount}, decrypted=${decrypted.length}, ns=${ns || '<none>'}`);
1800
2209
  return decrypted;
@@ -1816,11 +2225,27 @@ export class V2E2EECoordinator {
1816
2225
  const client = this.client;
1817
2226
  if (!client._v2Session)
1818
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
+ };
1819
2239
  const processed = [];
1820
2240
  const plaintextMessages = [];
1821
2241
  let complete = true;
1822
2242
  for (const rawMessage of messages) {
1823
- 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
+ }
1824
2249
  if (claim === 'seen') {
1825
2250
  processed.push(rawMessage);
1826
2251
  continue;
@@ -1859,13 +2284,16 @@ export class V2E2EECoordinator {
1859
2284
  complete = false;
1860
2285
  continue;
1861
2286
  }
2287
+ ensureCurrent();
1862
2288
  const publicMessage = client._delivery.publicVolatileMessage(plaintext);
1863
2289
  for (const key of ['storage', 'volatile_cursor', 'created_at', 'expires_at', 'persist']) {
1864
2290
  if (Object.prototype.hasOwnProperty.call(rawMessage, key))
1865
2291
  publicMessage[key] = rawMessage[key];
1866
2292
  }
2293
+ publicMessage.need_render = typeof rawMessage.need_render === 'boolean' ? rawMessage.need_render : publicMessage.need_render !== false;
1867
2294
  const appEvent = client._delivery.p2pAppEventForMessage(publicMessage);
1868
2295
  await client._publishAppEvent(appEvent.event, appEvent.payload, source);
2296
+ ensureCurrent();
1869
2297
  client._delivery.markVolatileSeen(rawMessage);
1870
2298
  processed.push(rawMessage);
1871
2299
  plaintextMessages.push(publicMessage);
@@ -1875,14 +2303,17 @@ export class V2E2EECoordinator {
1875
2303
  }
1876
2304
  }
1877
2305
  if (processed.length > 0) {
2306
+ ensureCurrent();
1878
2307
  await client._delivery.ackVolatileMessages(processed, true, advanceCursor && complete && processed.length === messages.length);
1879
2308
  }
1880
2309
  return plaintextMessages;
1881
2310
  }
1882
2311
  async confirmForwardP2PAck(upToSeq) {
1883
2312
  const ns = this.client._aid ? `p2p:${this.client._aid}` : '';
2313
+ const generation = Number(this.client._v2RuntimeGeneration ?? 0);
1884
2314
  try {
1885
2315
  const result = await this.ackV2ForForward(upToSeq);
2316
+ this.ensureDeliveryCurrent(ns, generation);
1886
2317
  const actualAckSeq = this.client._delivery.resolveForwardAckSeq(result, upToSeq);
1887
2318
  if (actualAckSeq < upToSeq) {
1888
2319
  throw new E2EEError(`message.v2.ack server ACK watermark ${actualAckSeq} is below requested ${upToSeq}`);
@@ -1890,7 +2321,9 @@ export class V2E2EECoordinator {
1890
2321
  this.clearForwardAck(ns, upToSeq);
1891
2322
  }
1892
2323
  catch (exc) {
1893
- this.recordForwardAck(ns, upToSeq);
2324
+ if (Number(this.client._v2RuntimeGeneration ?? 0) === generation
2325
+ && this.client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false)
2326
+ this.recordForwardAck(ns, upToSeq);
1894
2327
  throw exc;
1895
2328
  }
1896
2329
  }
@@ -1985,6 +2418,7 @@ export class V2E2EECoordinator {
1985
2418
  timestamp: opts?.timestamp,
1986
2419
  protectedHeaders: opts?.protectedHeaders,
1987
2420
  context: opts?.context,
2421
+ needRender: opts?.needRender,
1988
2422
  useCache,
1989
2423
  });
1990
2424
  const result = await client.call('group.v2.send', withExplicitGroupAid({
@@ -2001,6 +2435,7 @@ export class V2E2EECoordinator {
2001
2435
  payload,
2002
2436
  message_id: messageId,
2003
2437
  timestamp: opts?.timestamp,
2438
+ need_render: opts?.needRender ?? true,
2004
2439
  protected_headers: opts?.protectedHeaders,
2005
2440
  context: opts?.context,
2006
2441
  }, groupAid), result, true);
@@ -2016,6 +2451,7 @@ export class V2E2EECoordinator {
2016
2451
  payload,
2017
2452
  message_id: messageId,
2018
2453
  timestamp: opts?.timestamp,
2454
+ need_render: opts?.needRender ?? true,
2019
2455
  protected_headers: opts?.protectedHeaders,
2020
2456
  context: opts?.context,
2021
2457
  }, groupAid), result, true);
@@ -2140,6 +2576,7 @@ export class V2E2EECoordinator {
2140
2576
  timestamp: opts.timestamp,
2141
2577
  protectedHeaders: opts.protectedHeaders,
2142
2578
  context: opts.context,
2579
+ needRender: opts.needRender,
2143
2580
  }, stateCommitment);
2144
2581
  client._logMessageDebug('send-envelope', 'group.send.v2', 'group.send', {
2145
2582
  group_id: groupId,
@@ -2161,7 +2598,7 @@ export class V2E2EECoordinator {
2161
2598
  });
2162
2599
  return envelope;
2163
2600
  }
2164
- async pullGroupV2TailInternal(params, deferPublish = false) {
2601
+ async pullGroupV2TailInternal(params, deferPublish = false, namespaceLocked = false) {
2165
2602
  const client = this.client;
2166
2603
  const generation = Number(client._v2RuntimeGeneration ?? 0);
2167
2604
  if (params.window_mode !== 'tail')
@@ -2175,6 +2612,7 @@ export class V2E2EECoordinator {
2175
2612
  const afterSeq = strictWindowSeq(params.after_seq ?? 0, 'after_seq');
2176
2613
  const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
2177
2614
  const ns = `group:${groupId}`;
2615
+ const namespaceGeneration = client._delivery.captureSeqNamespaceGeneration?.(ns);
2178
2616
  const explicitCursorParams = isJsonObject(params._group_cursor_params)
2179
2617
  ? params._group_cursor_params : {};
2180
2618
  const cursorParams = Object.keys(explicitCursorParams).length > 0 ? explicitCursorParams : params;
@@ -2191,35 +2629,55 @@ export class V2E2EECoordinator {
2191
2629
  limit,
2192
2630
  _group_cursor_params: explicitCursorParams,
2193
2631
  }, `${ns}|tail|after=${afterSeq}|limit=${limit}`);
2194
- 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({
2195
2635
  group_id: groupId,
2196
2636
  window_mode: 'tail',
2197
2637
  after_seq: afterSeq,
2198
2638
  limit,
2199
2639
  _group_cursor_params: explicitCursorParams,
2200
2640
  _rpc_foreground: true,
2201
- }, groupAid), pullGateKey);
2641
+ }, groupAid), pullGateKey), false);
2202
2642
  const resultAid = String(result.group_aid ?? result.groupAid ?? '').trim();
2203
2643
  if (resultAid)
2204
2644
  groupAid = resultAid;
2205
2645
  const page = validateTailPage(result, afterSeq, limit);
2206
2646
  const decoded = [];
2207
- for (const msg of page.messages) {
2208
- this.ensureDeliveryCurrent(ns, generation);
2209
- client._logMessageDebug('tail-raw', 'group.v2.pull', 'group.message_created', msg);
2210
- const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, true, false, 'tail', deferPublish);
2211
- if (plaintext)
2212
- decoded.push(plaintext);
2213
- }
2214
- this.ensureDeliveryCurrent(ns, generation);
2215
- client._seqTracker.commitTailWindow(ns, {
2216
- windowStartSeq: page.windowStart,
2217
- headSeq: page.head,
2218
- coveredThroughSeq: page.head,
2219
- 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
+ }
2220
2655
  });
2221
- client._persistSeq(ns);
2222
- 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
+ }
2223
2681
  this.ensureDeliveryCurrent(ns, generation);
2224
2682
  const ack = client._seqTracker.getContiguousSeq(ns);
2225
2683
  if (ownsCursor)
@@ -2241,12 +2699,13 @@ export class V2E2EECoordinator {
2241
2699
  let groupAid = String(params.group_aid ?? '').trim();
2242
2700
  const beforeSeq = strictWindowSeq(params.before_seq, 'before_seq', true);
2243
2701
  const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
2244
- 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({
2245
2704
  group_id: groupId,
2246
2705
  before_seq: beforeSeq,
2247
2706
  limit,
2248
2707
  _rpc_foreground: true,
2249
- }, groupAid));
2708
+ }, groupAid), key), false);
2250
2709
  const resultAid = String(result.group_aid ?? result.groupAid ?? '').trim();
2251
2710
  if (resultAid)
2252
2711
  groupAid = resultAid;
@@ -2283,7 +2742,7 @@ export class V2E2EECoordinator {
2283
2742
  group_id: gid, window_mode: 'tail', after_seq: afterSeq, limit,
2284
2743
  _group_cursor_params: opts?.cursorParams,
2285
2744
  }, `${ns}|tail|after=${afterSeq}|limit=${limit}`);
2286
- return (await client._runPullSerialized(key, () => this.pullGroupV2Impl(gid, afterSeq, limit, {
2745
+ return (await client._rpcPipeline.runPullNamespaceSerialized(key, () => this.pullGroupV2Impl(gid, afterSeq, limit, {
2287
2746
  ...opts,
2288
2747
  gateLocked: true,
2289
2748
  }), false));
@@ -2296,7 +2755,7 @@ export class V2E2EECoordinator {
2296
2755
  window_mode: 'tail',
2297
2756
  after_seq: afterSeq,
2298
2757
  limit,
2299
- });
2758
+ }, false, true);
2300
2759
  return (Array.isArray(result.messages) ? result.messages : []);
2301
2760
  }
2302
2761
  if (!opts?.gateLocked) {
@@ -2304,20 +2763,17 @@ export class V2E2EECoordinator {
2304
2763
  group_id: gid, after_seq: afterSeq, force: opts?.force === true, limit,
2305
2764
  _group_cursor_params: opts?.cursorParams,
2306
2765
  }, `${ns}|forward|after=${afterSeq}|force=${opts?.force === true}|limit=${limit}`);
2307
- return await client._runPullSerialized(key, async () => this.pullGroupV2Impl(gid, afterSeq, limit, {
2766
+ return await client._rpcPipeline.runPullNamespaceSerialized(key, async () => this.pullGroupV2Impl(gid, afterSeq, limit, {
2308
2767
  ...(opts ?? {}),
2309
2768
  gateLocked: true,
2310
2769
  scheduleFollowup: true,
2311
- }), true);
2770
+ }), opts?.background !== false);
2312
2771
  }
2313
2772
  const decrypted = [];
2314
2773
  const wireGroupId = String(opts?.wireGroupId ?? groupId ?? '').trim() || gid;
2315
2774
  const cursorParams = opts?.cursorParams ?? {};
2316
2775
  const ownsCursor = opts?.ownsCursor !== false;
2317
- let pullGateKey = pullGateKeyForClient(client, 'group.v2.pull', {
2318
- group_id: gid, after_seq: afterSeq, force: opts?.force === true, limit,
2319
- _group_cursor_params: cursorParams,
2320
- }, `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
2776
+ const background = opts?.background !== false;
2321
2777
  let nextAfterSeq = (opts?.explicitAfterSeq || opts?.force)
2322
2778
  ? afterSeq
2323
2779
  : (afterSeq || client._seqTracker.getContiguousSeq(ns));
@@ -2328,13 +2784,27 @@ export class V2E2EECoordinator {
2328
2784
  let pageCount = 0;
2329
2785
  let lastAutoAckSeq = 0;
2330
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
+ };
2331
2802
  const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
2332
2803
  let pipelineRunning = false;
2333
2804
  let pipelineError = null;
2334
2805
  const pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
2335
2806
  let pipelineProducer = null;
2336
2807
  let pipelineStopped = false;
2337
- const pipelineResume = { current: null };
2338
2808
  const startPipeline = (startAfterSeq) => {
2339
2809
  if (pipelineRunning || pageCount >= maxPages || limit < V2_PULL_MAX_PAGE_LIMIT || !ownsCursor)
2340
2810
  return;
@@ -2344,35 +2814,29 @@ export class V2E2EECoordinator {
2344
2814
  for (let produced = pageCount; produced < maxPages; produced += 1) {
2345
2815
  if (pipelineStopped)
2346
2816
  break;
2347
- const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2348
- group_id: wireGroupId,
2349
- after_seq: producerAfterSeq,
2350
- limit,
2351
- ...cursorParams,
2352
- ...(opts?.force ? { force: true } : {}),
2353
- }, groupAid), pullGateKey);
2817
+ this.ensureDeliveryCurrent(ns, generation);
2818
+ const ackUpToSeq = Math.min(pendingAckSeq, committedAckSeq);
2819
+ const inlineAckGeneration = ackUpToSeq > 0 ? client._delivery.captureGroupInlineAckGeneration() : null;
2820
+ const result = await fetchPage(producerAfterSeq, ackUpToSeq);
2354
2821
  if (pipelineStopped)
2355
2822
  break;
2823
+ this.ensureDeliveryCurrent(ns, generation);
2356
2824
  const pageMessages = (Array.isArray(result.messages) ? result.messages : []);
2357
- const completed = new Promise((resolve) => { pipelineResume.current = resolve; });
2358
2825
  const enqueued = await pipelineQueue.put({
2359
2826
  result,
2360
2827
  afterSeq: producerAfterSeq,
2361
- ackUpToSeq: 0,
2362
- inlineAckGeneration: null,
2828
+ ackUpToSeq,
2829
+ inlineAckGeneration,
2363
2830
  requestedVolatileCursor: '',
2364
2831
  weight: pageMessages.length,
2365
2832
  }, pageMessages.length);
2366
2833
  if (!enqueued || pipelineStopped)
2367
2834
  break;
2368
- await completed;
2369
- if (pipelineStopped)
2370
- break;
2371
2835
  const messages = deduplicateForwardPageMessages(pageMessages, producerAfterSeq);
2372
2836
  const seqs = messages
2373
2837
  .map((msg) => Number(msg.seq ?? 0))
2374
2838
  .filter((seq) => Number.isFinite(seq) && seq > 0);
2375
- const nextAfter = Math.max(contiguousPullSeq(producerAfterSeq, seqs, result.contiguous_seq), producerAfterSeq);
2839
+ const nextAfter = Math.max(producerAfterSeq, ...seqs);
2376
2840
  const cursor = isJsonObject(result.cursor)
2377
2841
  ? result.cursor : null;
2378
2842
  const shouldContinue = shouldContinueForwardPage({
@@ -2398,8 +2862,6 @@ export class V2E2EECoordinator {
2398
2862
  };
2399
2863
  const stopPipeline = async () => {
2400
2864
  pipelineStopped = true;
2401
- pipelineResume.current?.();
2402
- pipelineResume.current = null;
2403
2865
  pipelineQueue.close();
2404
2866
  if (pipelineProducer) {
2405
2867
  const producer = pipelineProducer;
@@ -2422,7 +2884,7 @@ export class V2E2EECoordinator {
2422
2884
  }
2423
2885
  pageCount += 1;
2424
2886
  const ackUpToSeq = ownsCursor ? (pipelinePage?.ackUpToSeq ?? pendingAckSeq) : 0;
2425
- const inlineAckGeneration = pipelinePage?.inlineAckGeneration ?? (ackUpToSeq > 0
2887
+ const inlineAckGeneration = pipelinePage ? pipelinePage.inlineAckGeneration : (ackUpToSeq > 0
2426
2888
  ? client._delivery.captureGroupInlineAckGeneration()
2427
2889
  : null);
2428
2890
  client._clientLog.debug(`group.v2.pull page request: group=${gid}, page=${pageCount}, after_seq=${nextAfterSeq}, limit=${limit}, ack_up_to_seq=${ackUpToSeq}, ns=${ns}`);
@@ -2432,30 +2894,23 @@ export class V2E2EECoordinator {
2432
2894
  result = pipelinePage.result;
2433
2895
  }
2434
2896
  else {
2435
- result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2436
- group_id: wireGroupId,
2437
- after_seq: nextAfterSeq,
2438
- limit,
2439
- ...cursorParams,
2440
- ...(opts?.force ? { force: true } : {}),
2441
- ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
2442
- }, groupAid), pullGateKey);
2897
+ result = await fetchPage(nextAfterSeq, ackUpToSeq);
2443
2898
  }
2899
+ if (ownsCursor)
2900
+ this.ensureDeliveryCurrent(ns, generation);
2444
2901
  if (ackUpToSeq > 0) {
2445
2902
  const actualAckSeq = client._delivery.resolveGroupPullAckSeq(result, ackUpToSeq);
2446
2903
  if (actualAckSeq >= ackUpToSeq) {
2447
2904
  if (inlineAckGeneration !== null) {
2448
2905
  client._delivery.onGroupV2PullAckResult(gid, result, ackUpToSeq, inlineAckGeneration);
2449
2906
  }
2450
- pendingAckSeq = 0;
2907
+ if (pendingAckSeq <= ackUpToSeq)
2908
+ pendingAckSeq = 0;
2451
2909
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
2452
2910
  this.clearForwardAck(ns, ackUpToSeq);
2453
2911
  }
2454
2912
  else {
2455
2913
  client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
2456
- await this.confirmForwardGroupAck(gid, ackUpToSeq, groupAid);
2457
- pendingAckSeq = 0;
2458
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
2459
2914
  }
2460
2915
  }
2461
2916
  const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
@@ -2476,6 +2931,7 @@ export class V2E2EECoordinator {
2476
2931
  .map((msg) => Number(msg.seq ?? 0))
2477
2932
  .filter((seq) => Number.isFinite(seq) && seq > 0);
2478
2933
  const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
2934
+ const namespaceGeneration = client._delivery.captureSeqNamespaceGeneration?.(ns);
2479
2935
  let pageTrackerSnapshot = null;
2480
2936
  let trackerMutated = false;
2481
2937
  let pageMaxSeq = nextAfterSeq;
@@ -2483,33 +2939,76 @@ export class V2E2EECoordinator {
2483
2939
  pageMaxSeq = Math.max(...seqs);
2484
2940
  }
2485
2941
  const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
2942
+ if (!pipelineRunning && ownsCursor && shouldContinueForwardPage({
2943
+ rawCount: messages.length, nextAfterSeq, nextAfter: pageMaxSeq, limit,
2944
+ hasMoreField, hasMore, serverHead,
2945
+ observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._delivery.pendingPullUpper?.(ns) ?? 0)),
2946
+ pendingAckSeq: 0,
2947
+ }))
2948
+ startPipeline(pageMaxSeq);
2486
2949
  const deferredKeyFetches = new Map();
2487
2950
  let blockedSeq = 0;
2488
2951
  const pageDecryptedBefore = decrypted.length;
2489
- for (const msg of messages) {
2490
- const seq = Number(msg.seq ?? 0);
2491
- if (!Number.isFinite(seq) || seq <= 0)
2492
- continue;
2493
- const version = String(msg.version ?? 'v2');
2494
- if (version === 'v1') {
2495
- const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
2496
- const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
2497
- const payloadObj = isJsonObject(payload) ? payload : null;
2498
- const payloadType = payloadObj ? String(payloadObj.type ?? '').trim() : '';
2499
- // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
2500
- if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
2501
- && client._delivery.recallEventFromGroupMessage(msg)) {
2502
- await client._delivery.publishGroupRecallTombstone(gid, seq, {
2503
- ...msg,
2504
- group_id: gid,
2505
- group_aid: eventGroupAid,
2506
- }, 'pull');
2507
- client._markPublishedSeq(ns, seq);
2508
- 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)
2509
2963
  continue;
2510
- }
2511
- if (payloadObj) {
2512
- 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) {
2513
3012
  let v1Msg = {
2514
3013
  message_id: String(msg.message_id ?? ''),
2515
3014
  from: String(msg.from_aid ?? ''),
@@ -2520,114 +3019,81 @@ export class V2E2EECoordinator {
2520
3019
  timestamp: msg.t_server,
2521
3020
  payload,
2522
3021
  encrypted: false,
3022
+ need_render: msg.need_render !== false,
2523
3023
  };
2524
3024
  if (msg.sender_device_id !== undefined)
2525
3025
  v1Msg.sender_device_id = msg.sender_device_id;
2526
3026
  v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
2527
3027
  attachGatewayProximity(v1Msg, msg);
2528
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2529
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2530
- }
2531
- else {
2532
- client._markPublishedSeq(ns, seq);
2533
- }
3028
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2534
3029
  decrypted.push(v1Msg);
2535
3030
  client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
2536
3031
  continue;
2537
3032
  }
2538
- }
2539
- else if (payload !== undefined && payload !== null) {
2540
- let v1Msg = {
2541
- message_id: String(msg.message_id ?? ''),
2542
- from: String(msg.from_aid ?? ''),
2543
- group_id: gid,
2544
- group_aid: eventGroupAid,
2545
- seq: msg.seq,
2546
- type: String(msg.type ?? ''),
2547
- timestamp: msg.t_server,
2548
- payload,
2549
- encrypted: false,
2550
- };
2551
- if (msg.sender_device_id !== undefined)
2552
- v1Msg.sender_device_id = msg.sender_device_id;
2553
- v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
2554
- attachGatewayProximity(v1Msg, msg);
2555
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2556
- decrypted.push(v1Msg);
2557
- client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
2558
- continue;
2559
- }
2560
- if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
2561
- const deferStatus = {};
2562
- const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
2563
- if (deferStatus.deferred) {
2564
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2565
- if (deferStatus.fromAid) {
2566
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2567
- 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
+ }
2568
3042
  }
2569
- }
2570
- if (plaintext) {
2571
- plaintext.group_id = gid;
2572
- plaintext.group_aid = eventGroupAid;
2573
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2574
- await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
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);
2575
3054
  }
2576
- else {
2577
- client._markPublishedSeq(ns, seq);
2578
- }
2579
- decrypted.push(plaintext);
2580
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);
2581
3088
  }
2582
3089
  else {
2583
- 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);
2584
3091
  }
2585
- continue;
2586
- }
2587
- if (version !== 'v2') {
2588
- client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
2589
- continue;
2590
- }
2591
- const deferStatus = {};
2592
- let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
2593
- if (deferStatus.deferred) {
2594
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2595
- }
2596
- if (deferStatus.deferred && deferStatus.fromAid) {
2597
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2598
- deferredKeyFetches.set(key, {
2599
- fromAid: deferStatus.fromAid,
2600
- senderDeviceId: deferStatus.senderDeviceId ?? '',
2601
- groupId: deferStatus.groupId ?? '',
2602
- });
2603
- }
2604
- if (plaintext === null) {
2605
- client._clientLog.debug(`group.v2.pull decrypt returned null: group=${gid}, seq=${seq}`);
2606
- continue;
3092
+ decrypted.push(plaintext);
3093
+ client._logMessageDebug('decrypt-ok', 'group.v2.pull', 'group.message_created', plaintext);
2607
3094
  }
2608
- plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
2609
- plaintext.group_id = gid;
2610
- plaintext.group_aid = eventGroupAid;
2611
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2612
- await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2613
- }
2614
- else {
2615
- client._markPublishedSeq(ns, seq);
2616
- }
2617
- decrypted.push(plaintext);
2618
- client._logMessageDebug('decrypt-ok', 'group.v2.pull', 'group.message_created', plaintext);
2619
- }
2620
- const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
2621
- const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
2622
- const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
2623
- const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq'));
2624
- const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
2625
- const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
2626
- const commitTarget = Math.max(pageContigBefore, effectiveFloor, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, ownsCursor ? cursorCurrentSeq : 0, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
2627
- let ackSeq = pageContigBefore;
2628
- let contigAdvanced = false;
2629
- if (ownsCursor) {
2630
- try {
3095
+ commitTarget = Math.max(pageContigBefore, effectiveFloor, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, ownsCursor ? cursorCurrentSeq : 0, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
3096
+ if (ownsCursor) {
2631
3097
  this.ensureDeliveryCurrent(ns, generation);
2632
3098
  const window = client._seqTracker.getSyncState?.(ns);
2633
3099
  const deliveryTarget = window && commitTarget >= window.tail - 1
@@ -2636,6 +3102,16 @@ export class V2E2EECoordinator {
2636
3102
  await client._drainOrderedMessages(ns, undefined, false, false, 'pull', deliveryTarget);
2637
3103
  }
2638
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);
2639
3115
  pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
2640
3116
  if (commitTarget > pageContigBefore) {
2641
3117
  trackerMutated = true;
@@ -2655,12 +3131,16 @@ export class V2E2EECoordinator {
2655
3131
  if (contigAdvanced) {
2656
3132
  await client._commitSeqTrackerState(ns);
2657
3133
  this.ensureDeliveryCurrent(ns, generation);
3134
+ if (client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) === false)
3135
+ throw new Error('pull invalidated');
2658
3136
  if (pageCount === 1 && deferredServerCursor > 0)
2659
3137
  this.clearForwardCursor(ns, ackSeq);
2660
3138
  }
3139
+ committedAckSeq = ackSeq;
2661
3140
  }
2662
3141
  catch (exc) {
2663
3142
  if (trackerMutated && Number(client._v2RuntimeGeneration ?? 0) === generation
3143
+ && client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) !== false
2664
3144
  && client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false
2665
3145
  && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
2666
3146
  (client._seqTracker._restoreNamespaceSnapshotPreservingPush ?? client._seqTracker.restoreNamespaceSnapshot)
@@ -2688,18 +3168,8 @@ export class V2E2EECoordinator {
2688
3168
  || (contigAdvanced && ackSeq > knownServerCursorSeq));
2689
3169
  if (ackNeeded) {
2690
3170
  this.recordForwardAck(ns, ackSeq);
2691
- const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
2692
- const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
2693
- if (canContinuePage && !pipelineRunning) {
2694
- pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
2695
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
2696
- client._clientLog.debug(`group.v2.pull queued piggyback auto-ack: group=${gid}, ns=${ns}, ack_seq=${pendingAckSeq}, raw_count=${messages.length}`);
2697
- }
2698
- else {
2699
- client._clientLog.debug(`group.v2.pull sending terminal auto-ack: group=${gid}, ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
2700
- await this.confirmForwardGroupAck(gid, ackSeq, groupAid);
2701
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
2702
- }
3171
+ pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
3172
+ client._clientLog.debug(`group.v2.pull queued piggyback auto-ack: group=${gid}, ns=${ns}, ack_seq=${pendingAckSeq}, raw_count=${messages.length}`);
2703
3173
  }
2704
3174
  client._delivery.syncProgress(ns, {
2705
3175
  rawCount: messages.length,
@@ -2709,7 +3179,7 @@ export class V2E2EECoordinator {
2709
3179
  currentSeq: client._seqTracker.getContiguousSeq(ns),
2710
3180
  targetSeq: serverHead,
2711
3181
  });
2712
- const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
3182
+ const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
2713
3183
  if (!ownsCursor)
2714
3184
  break;
2715
3185
  const rawCount = messages.length;
@@ -2740,28 +3210,12 @@ export class V2E2EECoordinator {
2740
3210
  if (!shouldContinue)
2741
3211
  break;
2742
3212
  if (shouldContinueForPipeline && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
2743
- if (pendingAckSeq > 0) {
2744
- await this.confirmForwardGroupAck(gid, pendingAckSeq, groupAid);
2745
- pendingAckSeq = 0;
2746
- }
2747
3213
  startPipeline(nextAfter);
2748
3214
  }
2749
- if (!pipelineRunning && pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
2750
- const nextKey = pullGateKeyForClient(client, 'group.v2.pull', {
2751
- group_id: gid, after_seq: nextAfter, force: opts?.force === true, limit,
2752
- _group_cursor_params: cursorParams,
2753
- }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
2754
- await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
2755
- pullGateKey = nextKey;
2756
- }
2757
- const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
3215
+ const tailDelayMs = pipelineRunning ? 0 : v2PullTailDelayMs(rawCount, limit);
2758
3216
  if (tailDelayMs > 0)
2759
3217
  await v2Sleep(tailDelayMs);
2760
3218
  nextAfterSeq = nextAfter;
2761
- if (pipelinePage) {
2762
- pipelineResume.current?.();
2763
- pipelineResume.current = null;
2764
- }
2765
3219
  }
2766
3220
  }
2767
3221
  finally {
@@ -2769,11 +3223,14 @@ export class V2E2EECoordinator {
2769
3223
  }
2770
3224
  if (pageCount >= maxPages) {
2771
3225
  client._clientLog.warn(`group.v2.pull reached max_pages=${maxPages} group=${gid} after_seq=${nextAfterSeq}`);
2772
- if (pendingAckSeq > 0) {
2773
- client._clientLog.debug(`group.v2.pull flushing pending auto-ack after max_pages: group=${gid}, ns=${ns}, ack_seq=${pendingAckSeq}`);
2774
- await this.confirmForwardGroupAck(gid, pendingAckSeq, groupAid);
3226
+ }
3227
+ if (pendingAckSeq > 0) {
3228
+ this.ensureDeliveryCurrent(ns, generation);
3229
+ client._clientLog.debug(`group.v2.pull flushing pending auto-ack: group=${gid}, ns=${ns}, ack_seq=${pendingAckSeq}`);
3230
+ const ackToFlush = pendingAckSeq;
3231
+ await this.confirmForwardGroupAck(gid, ackToFlush, groupAid, background);
3232
+ if (pendingAckSeq <= ackToFlush)
2775
3233
  pendingAckSeq = 0;
2776
- }
2777
3234
  }
2778
3235
  client._clientLog.debug(`group.v2.pull done: group=${gid}, requested_after_seq=${afterSeq}, pages=${pageCount}, decrypted=${decrypted.length}, ns=${ns}`);
2779
3236
  return decrypted;
@@ -2792,10 +3249,12 @@ export class V2E2EECoordinator {
2792
3249
  throw error;
2793
3250
  }
2794
3251
  }
2795
- async confirmForwardGroupAck(groupId, upToSeq, groupAid) {
3252
+ async confirmForwardGroupAck(groupId, upToSeq, groupAid, background = false) {
2796
3253
  const ns = `group:${String(groupId ?? '').trim()}`;
3254
+ const generation = Number(this.client._v2RuntimeGeneration ?? 0);
2797
3255
  try {
2798
- const result = await this.ackGroupV2ForForward(groupId, upToSeq, groupAid);
3256
+ const result = await this.ackGroupV2ForForward(groupId, upToSeq, groupAid, background);
3257
+ this.ensureDeliveryCurrent(ns, generation);
2799
3258
  const actualAckSeq = this.client._delivery.resolveForwardAckSeq(result, upToSeq);
2800
3259
  if (actualAckSeq < upToSeq) {
2801
3260
  throw new E2EEError(`group.v2.ack server ACK watermark ${actualAckSeq} is below requested ${upToSeq}`);
@@ -2803,17 +3262,19 @@ export class V2E2EECoordinator {
2803
3262
  this.clearForwardAck(ns, upToSeq);
2804
3263
  }
2805
3264
  catch (exc) {
2806
- this.recordForwardAck(ns, upToSeq);
3265
+ if (Number(this.client._v2RuntimeGeneration ?? 0) === generation
3266
+ && this.client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false)
3267
+ this.recordForwardAck(ns, upToSeq);
2807
3268
  throw exc;
2808
3269
  }
2809
3270
  }
2810
3271
  async ackGroupV2(groupId, upToSeq, groupAid) {
2811
3272
  return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, false);
2812
3273
  }
2813
- async ackGroupV2ForForward(groupId, upToSeq, groupAid) {
2814
- return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, true);
3274
+ async ackGroupV2ForForward(groupId, upToSeq, groupAid, background = false) {
3275
+ return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, true, background);
2815
3276
  }
2816
- async ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, requireServerWatermark) {
3277
+ async ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, requireServerWatermark, background) {
2817
3278
  const client = this.client;
2818
3279
  const gid = String(groupId ?? '').trim();
2819
3280
  if (!gid)
@@ -2831,7 +3292,9 @@ export class V2E2EECoordinator {
2831
3292
  return { acked: 0 };
2832
3293
  const inlineAckGeneration = client._delivery.captureGroupInlineAckGeneration();
2833
3294
  client._clientLog.debug(`group.v2.ack send: group=${gid}, ns=${ns}, up_to_seq=${seq}`);
2834
- 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));
2835
3298
  const actualAckSeq = requireServerWatermark
2836
3299
  ? client._delivery.resolveForwardAckSeq(result, seq)
2837
3300
  : client._delivery.resolveActualAckSeq(result, seq);