@agentunion/fastaun 0.5.20 → 0.5.22

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 (128) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/_packed_docs/CHANGELOG.md +104 -0
  3. package/_packed_docs/INDEX.md +59 -34
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +59 -52
  5. package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/344/270/216/346/215/242owner/347/274/226/346/216/222/346/226/271/346/241/210.md +530 -0
  6. package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/345/256/214/346/225/264/350/256/276/350/256/241/344/270/216/345/256/236/346/226/275/350/256/241/345/210/222.md +685 -0
  7. package/_packed_docs/agent.md/working/agent.md-schema/344/270/216/347/255/276/345/217/221/351/252/214/347/255/276/346/265/201/347/250/213/346/276/204/346/270/205.md +349 -0
  8. package/_packed_docs/agent.md/working//346/234/254/345/234/260Docker/346/265/213/350/257/225Bug/346/270/205/345/215/225.md +40 -0
  9. package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +307 -67
  10. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +41 -15
  11. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1433 -1392
  12. package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
  13. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +30 -8
  14. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +2 -0
  15. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +33 -22
  16. 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
  17. 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
  18. package/_packed_docs/sdk/0.5.22-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +212 -0
  19. package/_packed_docs/sdk/0.5.22-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +199 -0
  20. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
  21. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
  22. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
  23. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +4 -1
  24. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +42 -39
  25. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +163 -14
  26. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +48 -47
  27. package/_packed_docs/sdk/09-group-rpc-manual.md +15 -2
  28. package/_packed_docs/sdk/09-message-rpc-manual.md +250 -160
  29. package/_packed_docs/sdk/09-storage-rpc-manual.md +64 -7
  30. package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +73 -0
  31. package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +90 -0
  32. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +33 -25
  33. package/_packed_docs/sdk/INDEX.md +67 -33
  34. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +74 -57
  35. 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 +312 -300
  36. 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 +322 -382
  37. package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -708
  38. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +28 -20
  39. 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 +190 -97
  40. 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 +158 -127
  41. package/dist/agent-md-schema.js +6 -4
  42. package/dist/agent-md-schema.js.map +1 -1
  43. package/dist/agent-md.d.ts +22 -2
  44. package/dist/agent-md.js +92 -68
  45. package/dist/agent-md.js.map +1 -1
  46. package/dist/aid-store.d.ts +13 -0
  47. package/dist/aid-store.js +50 -15
  48. package/dist/aid-store.js.map +1 -1
  49. package/dist/auth.d.ts +1 -0
  50. package/dist/auth.js +22 -2
  51. package/dist/auth.js.map +1 -1
  52. package/dist/cert-utils.d.ts +1 -0
  53. package/dist/cert-utils.js +13 -0
  54. package/dist/cert-utils.js.map +1 -1
  55. package/dist/client/delivery.d.ts +87 -1
  56. package/dist/client/delivery.js +1387 -139
  57. package/dist/client/delivery.js.map +1 -1
  58. package/dist/client/group-state.js +3 -0
  59. package/dist/client/group-state.js.map +1 -1
  60. package/dist/client/rpc-pipeline.d.ts +4 -1
  61. package/dist/client/rpc-pipeline.js +149 -37
  62. package/dist/client/rpc-pipeline.js.map +1 -1
  63. package/dist/client/v2-e2ee.d.ts +12 -2
  64. package/dist/client/v2-e2ee.js +831 -304
  65. package/dist/client/v2-e2ee.js.map +1 -1
  66. package/dist/client.d.ts +26 -2
  67. package/dist/client.js +412 -68
  68. package/dist/client.js.map +1 -1
  69. package/dist/config.d.ts +4 -0
  70. package/dist/config.js +1 -0
  71. package/dist/config.js.map +1 -1
  72. package/dist/events.d.ts +18 -1
  73. package/dist/events.js +148 -9
  74. package/dist/events.js.map +1 -1
  75. package/dist/facades.d.ts +43 -1
  76. package/dist/facades.js +363 -7
  77. package/dist/facades.js.map +1 -1
  78. package/dist/group-fs.d.ts +1 -0
  79. package/dist/group-fs.js +37 -19
  80. package/dist/group-fs.js.map +1 -1
  81. package/dist/index.d.ts +5 -3
  82. package/dist/index.js +1 -1
  83. package/dist/index.js.map +1 -1
  84. package/dist/keystore/aid-db.d.ts +6 -1
  85. package/dist/keystore/aid-db.js +184 -42
  86. package/dist/keystore/aid-db.js.map +1 -1
  87. package/dist/keystore/index.d.ts +14 -0
  88. package/dist/keystore/local-token-store.d.ts +9 -1
  89. package/dist/keystore/local-token-store.js +79 -14
  90. package/dist/keystore/local-token-store.js.map +1 -1
  91. package/dist/org-protocol.d.ts +46 -0
  92. package/dist/org-protocol.js +802 -12
  93. package/dist/org-protocol.js.map +1 -1
  94. package/dist/ownership-v21.d.ts +36 -5
  95. package/dist/ownership-v21.js +168 -52
  96. package/dist/ownership-v21.js.map +1 -1
  97. package/dist/ownership-workflow.d.ts +24 -2
  98. package/dist/ownership-workflow.js +682 -41
  99. package/dist/ownership-workflow.js.map +1 -1
  100. package/dist/secret-store/file-store.js +84 -0
  101. package/dist/secret-store/file-store.js.map +1 -1
  102. package/dist/seq-tracker.d.ts +1 -0
  103. package/dist/seq-tracker.js +2 -2
  104. package/dist/seq-tracker.js.map +1 -1
  105. package/dist/storage/lowlevel.d.ts +6 -0
  106. package/dist/storage/lowlevel.js +24 -7
  107. package/dist/storage/lowlevel.js.map +1 -1
  108. package/dist/storage/read-cache.d.ts +70 -0
  109. package/dist/storage/read-cache.js +625 -0
  110. package/dist/storage/read-cache.js.map +1 -0
  111. package/dist/storage/vfs.d.ts +2 -0
  112. package/dist/storage/vfs.js +50 -15
  113. package/dist/storage/vfs.js.map +1 -1
  114. package/dist/tools/cross-sdk-agent.d.ts +86 -3
  115. package/dist/tools/cross-sdk-agent.js +758 -46
  116. package/dist/tools/cross-sdk-agent.js.map +1 -1
  117. package/dist/transport.d.ts +3 -0
  118. package/dist/transport.js +104 -46
  119. package/dist/transport.js.map +1 -1
  120. package/dist/types.d.ts +1 -0
  121. package/dist/v2/e2ee/encrypt-group.js +1 -0
  122. package/dist/v2/e2ee/encrypt-group.js.map +1 -1
  123. package/dist/v2/e2ee/encrypt-p2p.js +1 -0
  124. package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
  125. package/dist/v2/e2ee/types.d.ts +1 -0
  126. package/dist/version.d.ts +1 -1
  127. package/dist/version.js +1 -1
  128. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import * as crypto from 'node:crypto';
2
+ import { _INTERNAL_MESSAGE_FLOW_LIMITS } from '../config.js';
2
3
  import { E2EEError, StateError, ValidationError } from '../errors.js';
3
4
  import { decryptMessage, encryptGroupMessage, encryptP2PMessage, } from '../v2/e2ee/index.js';
4
5
  import { V2Session } from '../v2/session/index.js';
@@ -349,6 +350,13 @@ export class V2E2EECoordinator {
349
350
  get client() {
350
351
  return this.runtime.client;
351
352
  }
353
+ async withApplicationPage(ns, source, operation, reservation, handlers) {
354
+ const delivery = this.client._delivery;
355
+ return typeof delivery.runApplicationPage === 'function'
356
+ ? await (handlers ? delivery.runApplicationPage(ns, source, operation, reservation, handlers)
357
+ : reservation ? delivery.runApplicationPage(ns, source, operation, reservation) : delivery.runApplicationPage(ns, source, operation))
358
+ : await operation();
359
+ }
352
360
  resetForwardCursorState() {
353
361
  this.pendingForwardCursors.clear();
354
362
  this.pendingForwardAcks.clear();
@@ -621,11 +629,13 @@ export class V2E2EECoordinator {
621
629
  return null;
622
630
  }
623
631
  }
624
- pendingSenderIKMessageKey(msg, groupId) {
632
+ pendingSenderIKMessageKey(msg, groupId, fromAid = '') {
625
633
  const client = this.client;
626
634
  const messageId = String(msg.message_id ?? '').trim();
627
635
  const seq = String(msg.seq ?? '').trim();
628
- const prefix = groupId ? `group:${groupId}` : `p2p:${client._aid ?? ''}`;
636
+ const prefix = groupId ? `group:${groupId}` : client._p2pPeerPullEnabled
637
+ ? client._delivery.peerNamespace(fromAid || String(msg.sender_aid ?? msg.from_aid ?? msg.from ?? ''))
638
+ : `p2p:${client._aid ?? ''}`;
629
639
  return `${prefix}:${messageId || seq || Math.random().toString(36).slice(2)}`;
630
640
  }
631
641
  pendingSenderIKFetchKey(fromAid, senderDeviceId, groupId) {
@@ -717,7 +727,7 @@ export class V2E2EECoordinator {
717
727
  return;
718
728
  const senderDeviceId = String(args.senderDeviceId ?? '');
719
729
  const groupId = String(args.groupId ?? '').trim();
720
- const messageKey = this.pendingSenderIKMessageKey(args.msg, groupId);
730
+ const messageKey = this.pendingSenderIKMessageKey(args.msg, groupId, fromAid);
721
731
  client._v2SenderIKPending.set(messageKey, {
722
732
  msg: { ...args.msg },
723
733
  fromAid,
@@ -833,10 +843,19 @@ export class V2E2EECoordinator {
833
843
  }
834
844
  plaintext = normalizeGroupMentionMode(plaintext, entry.msg, entry.msg.envelope_json);
835
845
  plaintext.group_id = entry.groupId;
846
+ plaintext.need_render = typeof entry.msg.need_render === 'boolean' ? entry.msg.need_render : plaintext.need_render !== false;
836
847
  await client._publishPulledMessage('group.message_created', `group:${entry.groupId}`, seq, plaintext, false, 'pending_retry');
837
848
  }
838
849
  else {
839
- await client._publishPulledMessage('message.received', `p2p:${client._aid ?? ''}`, seq, plaintext, false, 'pending_retry');
850
+ const ns = client._p2pPeerPullEnabled
851
+ ? await client._delivery.initializePeer(entry.fromAid) : `p2p:${client._aid ?? ''}`;
852
+ if (!generationIsCurrent())
853
+ return;
854
+ plaintext.sender_aid = entry.fromAid;
855
+ plaintext.need_render = typeof entry.msg.need_render === 'boolean' ? entry.msg.need_render : plaintext.need_render !== false;
856
+ await client._publishPulledMessage('message.received', ns, seq, plaintext, false, 'pending_retry');
857
+ if (client._p2pPeerPullEnabled && generationIsCurrent())
858
+ client._delivery.schedulePeerRecovery(entry.fromAid, seq);
840
859
  }
841
860
  if (generationIsCurrent())
842
861
  pending.delete(key);
@@ -852,6 +871,8 @@ export class V2E2EECoordinator {
852
871
  }
853
872
  async publishPendingV2VolatileMessage(rawMessage, plaintext, source) {
854
873
  const delivery = this.client._delivery;
874
+ const generation = Number(this.client._v2RuntimeGeneration ?? 0);
875
+ const context = JSON.stringify([this.client._aid, this.client._deviceId, this.client._slotId]);
855
876
  const claim = delivery.beginVolatileDelivery(rawMessage);
856
877
  if (claim === 'inflight')
857
878
  return;
@@ -865,8 +886,14 @@ export class V2E2EECoordinator {
865
886
  if (Object.prototype.hasOwnProperty.call(rawMessage, key))
866
887
  publicMessage[key] = rawMessage[key];
867
888
  }
889
+ publicMessage.need_render = typeof rawMessage.need_render === 'boolean' ? rawMessage.need_render : plaintext.need_render !== false;
868
890
  const appEvent = delivery.p2pAppEventForMessage(publicMessage);
869
891
  await this.client._publishAppEvent(appEvent.event, appEvent.payload, source);
892
+ if (this.client._p2pPeerPullEnabled) {
893
+ if (context !== JSON.stringify([this.client._aid, this.client._deviceId, this.client._slotId]))
894
+ throw new StateError('delivery invalidated');
895
+ this.ensureDeliveryCurrent(delivery.peerNamespace(String(rawMessage.sender_aid ?? rawMessage.from_aid ?? rawMessage.from ?? '')), generation);
896
+ }
870
897
  delivery.markVolatileSeen(rawMessage);
871
898
  }
872
899
  finally {
@@ -1040,6 +1067,7 @@ export class V2E2EECoordinator {
1040
1067
  timestamp: opts.timestamp,
1041
1068
  protectedHeaders: opts.protectedHeaders,
1042
1069
  context: opts.context,
1070
+ needRender: opts.needRender,
1043
1071
  });
1044
1072
  client._logMessageDebug('send-envelope', 'message.send.v2', 'message.send', {
1045
1073
  message_id: envelope.message_id,
@@ -1084,6 +1112,7 @@ export class V2E2EECoordinator {
1084
1112
  deviceId: targetDeviceId || undefined,
1085
1113
  protectedHeaders: opts?.protectedHeaders,
1086
1114
  context: opts?.context,
1115
+ needRender: opts?.needRender,
1087
1116
  useCache,
1088
1117
  });
1089
1118
  const result = await client.call('message.send', {
@@ -1093,6 +1122,7 @@ export class V2E2EECoordinator {
1093
1122
  ...(targetDeviceId ? { device_id: targetDeviceId } : {}),
1094
1123
  ...(opts?.ttl !== undefined ? { ttl: opts.ttl } : {}),
1095
1124
  encrypt: false,
1125
+ need_render: opts?.needRender ?? true,
1096
1126
  _skip_send_result_envelope: true,
1097
1127
  });
1098
1128
  client._clientLog.debug(`message.v2.send ok: to=${toAid}, use_cache=${useCache}, seq=${String((isJsonObject(result) ? result.seq : '') ?? '')}`);
@@ -1106,6 +1136,7 @@ export class V2E2EECoordinator {
1106
1136
  message_id: messageId,
1107
1137
  timestamp: opts?.timestamp,
1108
1138
  ttl: opts?.ttl,
1139
+ need_render: opts?.needRender ?? true,
1109
1140
  protected_headers: opts?.protectedHeaders,
1110
1141
  context: opts?.context,
1111
1142
  }, result, true);
@@ -1122,6 +1153,7 @@ export class V2E2EECoordinator {
1122
1153
  message_id: messageId,
1123
1154
  timestamp: opts?.timestamp,
1124
1155
  ttl: opts?.ttl,
1156
+ need_render: opts?.needRender ?? true,
1125
1157
  protected_headers: opts?.protectedHeaders,
1126
1158
  context: opts?.context,
1127
1159
  }, result, true);
@@ -1137,12 +1169,13 @@ export class V2E2EECoordinator {
1137
1169
  history_error: { code: 'decrypt_failed', message: reason },
1138
1170
  };
1139
1171
  }
1140
- ensureDeliveryCurrent(ns, generation) {
1172
+ ensureDeliveryCurrent(ns, generation, inlineGeneration) {
1141
1173
  if (Number(this.client._v2RuntimeGeneration ?? 0) !== generation
1174
+ || (inlineGeneration !== undefined && this.client._delivery.captureInlineAckGeneration() !== inlineGeneration)
1142
1175
  || this.client._rpcPipeline?.isPullNamespaceCurrent?.(ns) === false)
1143
1176
  throw new StateError('pull invalidated');
1144
1177
  }
1145
- async decodeP2PWindowMessage(msg, publish, history, source, orderedPublish = false) {
1178
+ async decodeP2PWindowMessage(msg, publish, history, source, orderedPublish = false, deferredEffects) {
1146
1179
  const client = this.client;
1147
1180
  const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
1148
1181
  let plaintext = null;
@@ -1173,11 +1206,11 @@ export class V2E2EECoordinator {
1173
1206
  else if (isJsonObject(payload)
1174
1207
  && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1175
1208
  msg = { ...msg, envelope_json: JSON.stringify(payload) };
1176
- plaintext = await client._decryptV2Message(msg, false, !history, true, true, undefined, false, source);
1209
+ plaintext = await client._decryptV2Message(msg, false, !history, true, true, undefined, false, source, ...(deferredEffects ? [deferredEffects] : []));
1177
1210
  }
1178
1211
  }
1179
1212
  else {
1180
- plaintext = await client._decryptV2Message(msg, false, !history, true, true, undefined, false, source);
1213
+ plaintext = await client._decryptV2Message(msg, false, !history, true, true, undefined, false, source, ...(deferredEffects ? [deferredEffects] : []));
1181
1214
  }
1182
1215
  if (!plaintext) {
1183
1216
  if (history)
@@ -1260,6 +1293,7 @@ export class V2E2EECoordinator {
1260
1293
  client._clientLog.warn(`Group Tail 单条消息解密失败,连续性证明仍保留: group=${groupId}, seq=${seq}`);
1261
1294
  return null;
1262
1295
  }
1296
+ plaintext.need_render = typeof msg.need_render === 'boolean' ? msg.need_render : plaintext.need_render !== false;
1263
1297
  if (publish && !history && client._delivery.isSelfSentGroupMessage?.(msg)) {
1264
1298
  return plaintext;
1265
1299
  }
@@ -1268,6 +1302,415 @@ export class V2E2EECoordinator {
1268
1302
  }
1269
1303
  return plaintext;
1270
1304
  }
1305
+ async pullPeerV2(senderAid, opts = {}) {
1306
+ const client = this.client;
1307
+ const generation = Number(client._v2RuntimeGeneration ?? 0);
1308
+ const inlineGeneration = client._delivery.captureInlineAckGeneration?.();
1309
+ const pullVersion = opts.peerPullVersion ?? client._delivery.capturePeerTailVersion(senderAid);
1310
+ await client._ensureV2SessionReady('message.pull');
1311
+ this.ensureDeliveryCurrent(client._delivery.peerNamespace(senderAid), generation, inlineGeneration);
1312
+ const ns = await client._delivery.initializePeer(senderAid);
1313
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1314
+ const limit = Number(opts.limit ?? _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize);
1315
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200)
1316
+ throw new ValidationError('peer limit must be between 1 and 200');
1317
+ let mode = opts.window_mode === 'tail' ? 'tail' : 'forward';
1318
+ let source = mode === 'tail' ? 'tail' : 'pull';
1319
+ if (mode === 'tail' && (opts.force === true || 'ack_up_to_seq' in opts))
1320
+ throw new ValidationError('Tail does not accept force or ACK');
1321
+ const budget = opts.budget ?? { remaining: Math.max(1, Number(opts.max_pages ?? 100)) };
1322
+ let after = opts.after_seq === undefined ? client._seqTracker.getContiguousSeq(ns) : strictWindowSeq(opts.after_seq, 'after_seq');
1323
+ if (mode !== 'tail' && opts.force !== true && after > client._seqTracker.getContiguousSeq(ns))
1324
+ throw new ValidationError('after_seq cannot exceed the peer delivery cursor');
1325
+ let volatileCursor = String(opts.after_volatile_cursor ?? client._delivery.peerVolatilePullCursor(senderAid));
1326
+ const decoded = [];
1327
+ let complete = false;
1328
+ let head = after;
1329
+ let tailVolatileHasMore;
1330
+ let tailServerAck = null;
1331
+ let pages = 0;
1332
+ let durableAck = 0;
1333
+ let initializedAck = false;
1334
+ let confirmedAck = 0;
1335
+ const confirmAck = (seq) => {
1336
+ confirmedAck = Math.max(confirmedAck, Math.min(seq, durableAck));
1337
+ this.clearForwardAck(ns, confirmedAck);
1338
+ };
1339
+ const prefetched = [];
1340
+ const stopping = new AbortController();
1341
+ const canPull = () => !opts.background || client._delivery.hasP2pApplicationSubscription();
1342
+ const validatePage = (response, requestAfter, committedAck, preparing = false) => {
1343
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1344
+ this.validatePeerResponse(response, senderAid);
1345
+ const rows = Array.isArray(response.messages) ? response.messages : null;
1346
+ if (!rows || rows.length > limit)
1347
+ throw new ValidationError('invalid peer messages page');
1348
+ const persistent = rows.filter((row) => row.storage !== 'volatile');
1349
+ const volatile = rows.filter((row) => row.storage === 'volatile');
1350
+ const end = strictWindowSeq(response.covered_through_seq, 'covered_through_seq');
1351
+ const pageHead = strictWindowSeq(response.head_seq, 'head_seq');
1352
+ let start;
1353
+ if (mode === 'tail') {
1354
+ if (response.window_mode !== 'tail')
1355
+ throw new ValidationError('peer Tail response mode mismatch');
1356
+ start = strictWindowSeq(response.window_start_seq, 'window_start_seq');
1357
+ if (volatile.length > 0 || start > end || end !== pageHead || (pageHead <= requestAfter
1358
+ ? start !== pageHead || persistent.length > 0
1359
+ : start <= requestAfter))
1360
+ throw new ValidationError('invalid peer Tail proof');
1361
+ if (response.volatile_has_more !== undefined && typeof response.volatile_has_more !== 'boolean')
1362
+ throw new ValidationError('invalid peer Tail volatile proof');
1363
+ }
1364
+ else {
1365
+ start = strictWindowSeq(response.scan_after_seq, 'scan_after_seq');
1366
+ const retention = safeNonNegativeSeq(response.retention_floor_seq) ?? 0;
1367
+ if (start < requestAfter || (!preparing && start > Math.max(requestAfter, retention, opts.force === true ? 0 : committedAck))
1368
+ || end < start || end > pageHead || response.next_after_seq !== end)
1369
+ throw new ValidationError('invalid peer Forward proof');
1370
+ if (typeof response.persistent_has_more !== 'boolean' || typeof response.volatile_has_more !== 'boolean'
1371
+ || response.has_more !== (response.persistent_has_more || response.volatile_has_more)
1372
+ || (!response.persistent_has_more && end < pageHead))
1373
+ throw new ValidationError('invalid peer continuation proof');
1374
+ }
1375
+ let previous = mode === 'tail' ? start - 1 : start;
1376
+ for (const row of persistent) {
1377
+ const seq = strictWindowSeq(row.seq, 'messages[].seq', true);
1378
+ if (seq <= previous || seq > end || String(row.sender_aid ?? row.from_aid ?? row.from ?? '') !== senderAid)
1379
+ throw new ValidationError('peer message is outside proof or sender');
1380
+ previous = seq;
1381
+ }
1382
+ if (mode === 'forward' && opts.force !== true) {
1383
+ const serverAck = safeNonNegativeSeq(response.server_ack_seq);
1384
+ if (serverAck !== null)
1385
+ client._delivery.observePeerServerAck(ns, serverAck);
1386
+ confirmAck(serverAck ?? 0);
1387
+ }
1388
+ return { persistent, volatile, end, pageHead, start };
1389
+ };
1390
+ const decodeRows = async (rows) => {
1391
+ const prepared = { messages: [], effects: [] };
1392
+ try {
1393
+ for (const row of rows) {
1394
+ if (stopping.signal.aborted)
1395
+ throw new StateError('pull invalidated');
1396
+ const plaintext = await this.decodeP2PWindowMessage(row, false, false, source, false, prepared.effects);
1397
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1398
+ if (!plaintext)
1399
+ throw new E2EEError('peer message could not be decrypted');
1400
+ plaintext.sender_aid = senderAid;
1401
+ plaintext.need_render = typeof row.need_render === 'boolean' ? row.need_render : plaintext.need_render !== false;
1402
+ prepared.messages.push(plaintext);
1403
+ }
1404
+ }
1405
+ catch (error) {
1406
+ prepared.error = error;
1407
+ }
1408
+ return prepared;
1409
+ };
1410
+ const fetchPage = async (requestAfter, requestCursor) => {
1411
+ const key = `${ns}|mode=${mode}|after=${requestAfter}|volatile=${requestCursor}|force=${Boolean(opts.force)}|limit=${limit}`;
1412
+ const request = (cursor) => {
1413
+ const operation = async () => {
1414
+ if (!canPull()) {
1415
+ reservation?.release();
1416
+ return null;
1417
+ }
1418
+ return { response: await client._callRawV2Rpc('message.v2.pull', {
1419
+ cursor_scope: 'peer', sender_aid: senderAid, device_id: client._deviceId, slot_id: client._slotId,
1420
+ after_seq: requestAfter, limit,
1421
+ ...(mode === 'forward' && opts.force !== true && durableAck > 0 ? { ack_up_to_seq: durableAck } : {}),
1422
+ ...(mode === 'tail' ? { window_mode: 'tail' } : {}),
1423
+ ...(opts.force === true ? { force: true } : {}),
1424
+ ...(cursor ? { after_volatile_cursor: cursor } : {}),
1425
+ ...(opts.background ? { _rpc_background: true } : { _rpc_foreground: true }),
1426
+ }, key), reservation };
1427
+ };
1428
+ const schedule = (attempt) => client._runPullSerialized(key, attempt, Boolean(opts.background));
1429
+ return client._delivery.runPeerPullAttempt?.(ns, operation, schedule) ?? schedule(operation);
1430
+ };
1431
+ const reservation = await client._dispatcher?._reserveMessageCapacity?.(limit, stopping.signal);
1432
+ try {
1433
+ if (stopping.signal.aborted)
1434
+ throw new StateError('pull invalidated');
1435
+ try {
1436
+ return await request(requestCursor);
1437
+ }
1438
+ catch (error) {
1439
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1440
+ if (!requestCursor || !String(error instanceof Error ? error.message : error).toLowerCase().includes('volatile cursor epoch mismatch'))
1441
+ throw error;
1442
+ client._delivery.setPeerVolatilePullCursor(senderAid, '');
1443
+ return await request('');
1444
+ }
1445
+ }
1446
+ catch (error) {
1447
+ reservation?.release();
1448
+ throw error;
1449
+ }
1450
+ };
1451
+ const prefetchPage = (requestAfter, cursor) => {
1452
+ const existing = prefetched.find((page) => page.after === requestAfter && page.cursor === cursor);
1453
+ if (existing)
1454
+ return existing;
1455
+ if (prefetched.length >= 2 || budget.remaining <= 0 || stopping.signal.aborted || !canPull())
1456
+ return undefined;
1457
+ budget.remaining -= 1;
1458
+ const page = { after: requestAfter, cursor, response: fetchPage(requestAfter, cursor) };
1459
+ void page.response.catch(() => undefined);
1460
+ prefetched.push(page);
1461
+ return page;
1462
+ };
1463
+ const preparePage = (page) => {
1464
+ if (page.prepared)
1465
+ return;
1466
+ page.prepared = page.response.then(async (fetched) => {
1467
+ if (!fetched)
1468
+ return null;
1469
+ try {
1470
+ const proof = validatePage(fetched.response, page.after, durableAck, true);
1471
+ const handlers = client._delivery.snapshotApplicationHandlers?.();
1472
+ if (proof.volatile.length === 0 && proof.end > page.after && fetched.response.persistent_has_more === true) {
1473
+ prefetchPage(proof.end, page.cursor);
1474
+ }
1475
+ return { ...fetched, handlers, prepared: await decodeRows(proof.persistent) };
1476
+ }
1477
+ catch (error) {
1478
+ fetched.reservation?.release();
1479
+ throw error;
1480
+ }
1481
+ });
1482
+ void page.prepared.catch(() => undefined);
1483
+ };
1484
+ client._delivery.syncStarted(ns);
1485
+ let failed = false;
1486
+ try {
1487
+ while (budget.remaining > 0 || prefetched.length > 0) {
1488
+ if (prefetched.length === 0 && !canPull())
1489
+ break;
1490
+ const cached = prefetched.shift();
1491
+ if (!cached)
1492
+ budget.remaining -= 1;
1493
+ pages += 1;
1494
+ let requestedAfter = cached ? cached.after : after;
1495
+ const requestedVolatile = cached ? cached.cursor : volatileCursor;
1496
+ const page = await client._delivery.runPeerPageSerialized(ns, async () => {
1497
+ const committedAck = client._delivery.peerCommittedAck(ns);
1498
+ if (!initializedAck) {
1499
+ durableAck = committedAck;
1500
+ initializedAck = true;
1501
+ }
1502
+ if (!cached && mode === 'tail' && opts.autoTail
1503
+ && !client._delivery.shouldTailPeer(ns, opts.autoTail.head, limit, opts.autoTail.forwardNeed)) {
1504
+ mode = 'forward';
1505
+ source = 'pull';
1506
+ opts.autoTail.forward = true;
1507
+ }
1508
+ if (!cached && mode === 'forward' && opts.force !== true)
1509
+ requestedAfter = Math.max(requestedAfter, committedAck);
1510
+ if (!cached)
1511
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1512
+ const fetched = await (cached ? cached.prepared ?? cached.response : fetchPage(requestedAfter, requestedVolatile));
1513
+ if (!fetched)
1514
+ return null;
1515
+ const { response, reservation } = fetched;
1516
+ try {
1517
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1518
+ const { persistent, volatile, end, pageHead, start } = validatePage(response, requestedAfter, committedAck);
1519
+ const handlers = fetched.handlers ?? client._delivery.snapshotApplicationHandlers?.();
1520
+ if (mode === 'forward' && opts.force !== true && volatile.length === 0 && end > requestedAfter
1521
+ && (response.persistent_has_more === true || response.has_more === true)) {
1522
+ const pending = prefetchPage(end, requestedVolatile);
1523
+ if (pending)
1524
+ preparePage(pending);
1525
+ }
1526
+ const messages = [];
1527
+ const prepared = fetched.prepared ?? await decodeRows(persistent);
1528
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1529
+ for (const effect of prepared.effects)
1530
+ effect();
1531
+ if ('error' in prepared)
1532
+ throw prepared.error;
1533
+ await this.withApplicationPage(ns, source, async () => {
1534
+ for (let index = 0; index < persistent.length; index += 1) {
1535
+ const row = persistent[index], plaintext = prepared.messages[index];
1536
+ const event = client._delivery.p2pAppEventForMessage(plaintext);
1537
+ await client._delivery.publishPulledMessage(event.event, ns, row.seq, event.payload, false, source);
1538
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1539
+ messages.push(plaintext);
1540
+ }
1541
+ if (volatile.length > 0) {
1542
+ for (const row of volatile)
1543
+ if (String(row.sender_aid ?? row.from_aid ?? row.from ?? '') !== senderAid)
1544
+ throw new ValidationError('volatile peer sender mismatch');
1545
+ messages.push(...await this.publishV2VolatileMessages(volatile, source, true));
1546
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1547
+ }
1548
+ }, reservation, handlers);
1549
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1550
+ if (!volatile.every((row) => client._delivery.isVolatileSeen(row)))
1551
+ throw new E2EEError('volatile peer delivery remains pending');
1552
+ const before = client._seqTracker.snapshotNamespace(ns);
1553
+ try {
1554
+ if (mode === 'tail') {
1555
+ if (pageHead > requestedAfter)
1556
+ client._seqTracker.commitTailWindow(ns, { windowStartSeq: start, headSeq: pageHead, coveredThroughSeq: end, pageLimit: limit, sparse: true });
1557
+ }
1558
+ else if (opts.force !== true) {
1559
+ client._seqTracker.onPullResult(ns, persistent, requestedAfter, end);
1560
+ }
1561
+ if (opts.force !== true) {
1562
+ await client._commitSeqTrackerState(ns);
1563
+ durableAck = Math.max(durableAck, client._seqTracker.getContiguousSeq(ns));
1564
+ }
1565
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1566
+ }
1567
+ catch (error) {
1568
+ if (Number(client._v2RuntimeGeneration ?? 0) === generation
1569
+ && (inlineGeneration === undefined || client._delivery.captureInlineAckGeneration() === inlineGeneration)
1570
+ && client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false)
1571
+ client._seqTracker._restoreNamespaceSnapshotPreservingPush(ns, before);
1572
+ throw error;
1573
+ }
1574
+ const ack = client._seqTracker.getContiguousSeq(ns);
1575
+ if (mode === 'forward' && opts.force !== true) {
1576
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1577
+ const next = safeNonNegativeSeq(response.next_pending_seq);
1578
+ client._delivery.consumePeerPullHint(ns, end, response.persistent_has_more === true && next !== null && next > end && next <= pageHead ? next : undefined);
1579
+ confirmAck(Math.max(confirmedAck, safeNonNegativeSeq(response.server_ack_seq) ?? 0));
1580
+ if (ack > confirmedAck)
1581
+ this.recordForwardAck(ns, ack);
1582
+ }
1583
+ return { response, messages, end, head: pageHead };
1584
+ }
1585
+ finally {
1586
+ reservation?.release();
1587
+ }
1588
+ });
1589
+ if (!page)
1590
+ break;
1591
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1592
+ if (opts.retainMessages !== false)
1593
+ decoded.push(...page.messages);
1594
+ head = page.head;
1595
+ if (mode === 'tail') {
1596
+ tailVolatileHasMore = page.response.volatile_has_more;
1597
+ tailServerAck = safeNonNegativeSeq(page.response.server_ack_seq);
1598
+ if (tailServerAck !== null)
1599
+ client._delivery.observePeerServerAck(ns, tailServerAck);
1600
+ }
1601
+ volatileCursor = client._delivery.peerVolatilePullCursor(senderAid);
1602
+ const more = page.response.persistent_has_more === true || page.response.volatile_has_more === true || page.response.has_more === true;
1603
+ complete = !more;
1604
+ client._delivery.syncProgress(ns, { rawCount: page.response.messages.length, pulledCount: page.messages.length, hasMore: more,
1605
+ currentSeq: client._seqTracker.getContiguousSeq(ns), targetSeq: head });
1606
+ after = page.end;
1607
+ if (mode === 'tail' || complete || (after <= requestedAfter && volatileCursor === requestedVolatile))
1608
+ break;
1609
+ }
1610
+ }
1611
+ catch (error) {
1612
+ failed = true;
1613
+ throw error;
1614
+ }
1615
+ finally {
1616
+ stopping.abort();
1617
+ for (const pending of prefetched) {
1618
+ try {
1619
+ (await (pending.prepared ?? pending.response))?.reservation?.release();
1620
+ }
1621
+ catch { }
1622
+ }
1623
+ if (mode === 'forward' && opts.force !== true) {
1624
+ try {
1625
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1626
+ const pending = this.pendingForwardAck(ns, durableAck);
1627
+ if (pending > 0) {
1628
+ await this.ackPeerV2(senderAid, pending);
1629
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1630
+ confirmAck(pending);
1631
+ }
1632
+ }
1633
+ catch (error) {
1634
+ if (!failed)
1635
+ throw error;
1636
+ }
1637
+ }
1638
+ }
1639
+ const ack = client._seqTracker.getContiguousSeq(ns);
1640
+ if (mode === 'tail')
1641
+ complete = ack >= head && durableAck >= head && tailVolatileHasMore === false
1642
+ && tailServerAck !== null && tailServerAck >= durableAck && this.pendingForwardAck(ns, durableAck) === 0;
1643
+ if (opts.force !== true && complete)
1644
+ complete = client._delivery.isPeerTailCurrent(senderAid, pullVersion, Math.min(ack, durableAck));
1645
+ if (!opts.background && opts.force !== true && !complete)
1646
+ client._delivery.schedulePeerRecovery(senderAid, head, true);
1647
+ return { sender_aid: senderAid, messages: decoded, complete,
1648
+ ack_seq: ack, head_seq: head, pages,
1649
+ ...(mode === 'tail' ? { ...(tailVolatileHasMore === undefined ? {} : { volatile_has_more: tailVolatileHasMore }),
1650
+ ...(tailServerAck === null ? {} : { server_ack_seq: tailServerAck }), pending_ack_seq: this.pendingForwardAck(ns, durableAck) } : {}) };
1651
+ }
1652
+ validatePeerResponse(response, senderAid) {
1653
+ const client = this.client;
1654
+ if (!response || response.cursor_scope !== 'peer' || response.sender_aid !== senderAid
1655
+ || (response.device_id !== undefined && response.device_id !== client._deviceId)
1656
+ || (response.slot_id !== undefined && response.slot_id !== client._slotId))
1657
+ throw new ValidationError('peer response scope mismatch');
1658
+ }
1659
+ async ackPeerV2(senderAid, upToSeq, volatileCursors = [], volatileIds = []) {
1660
+ const client = this.client;
1661
+ const generation = Number(client._v2RuntimeGeneration ?? 0);
1662
+ const inlineGeneration = client._delivery.captureInlineAckGeneration?.();
1663
+ const ns = await client._delivery.initializePeer(senderAid);
1664
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1665
+ const localAck = client._delivery.peerCommittedAck(ns);
1666
+ const requested = upToSeq === undefined ? (volatileCursors.length || volatileIds.length ? 0 : localAck) : strictWindowSeq(upToSeq, 'up_to_seq');
1667
+ const seq = Math.min(requested, localAck);
1668
+ if (!seq && !volatileCursors.length && !volatileIds.length)
1669
+ return { cursor_scope: 'peer', sender_aid: senderAid, peer_ack_seq: 0 };
1670
+ const result = await client._callRawV2Rpc('message.v2.ack', { cursor_scope: 'peer', sender_aid: senderAid,
1671
+ device_id: client._deviceId, slot_id: client._slotId,
1672
+ ...(seq ? { up_to_seq: seq } : {}), ...(volatileCursors.length ? { volatile_cursors: volatileCursors } : {}),
1673
+ ...(volatileIds.length ? { volatile_ids: volatileIds } : {}), _rpc_background: true });
1674
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1675
+ this.validatePeerResponse(result, senderAid);
1676
+ const actual = safeNonNegativeSeq(result.peer_ack_seq ?? result.effective_ack_seq ?? result.up_to_seq);
1677
+ if (seq > 0 && (actual === null || actual < seq))
1678
+ throw new E2EEError('peer ACK watermark is below requested');
1679
+ if (actual !== null)
1680
+ client._delivery.observePeerServerAck(ns, actual);
1681
+ client._delivery.peerAckChanged();
1682
+ return result;
1683
+ }
1684
+ async peerHistoryV2(params) {
1685
+ const sender = String(params.sender_aid ?? '').trim();
1686
+ if (!sender)
1687
+ throw new ValidationError('peer History requires sender_aid');
1688
+ const client = this.client;
1689
+ const ns = await client._delivery.initializePeer(sender);
1690
+ const generation = Number(client._v2RuntimeGeneration ?? 0);
1691
+ const before = strictWindowSeq(params.before_seq, 'before_seq', true);
1692
+ const limit = Number(params.limit ?? _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize);
1693
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200)
1694
+ throw new ValidationError('peer limit must be between 1 and 200');
1695
+ const key = pullGateKeyForClient(client, 'message.history', { ...params, sender_aid: sender, before_seq: before, limit }, `${ns}|history|before=${before}|limit=${limit}`);
1696
+ const result = await client._runPullSerialized(key, () => client._callRawV2Rpc('message.history', { ...params, cursor_scope: 'peer', sender_aid: sender,
1697
+ before_seq: before, limit, device_id: client._deviceId, slot_id: client._slotId, _rpc_foreground: true }, key), false);
1698
+ this.ensureDeliveryCurrent(ns, generation);
1699
+ this.validatePeerResponse(result, sender);
1700
+ const rows = validateHistoryPage(result, before);
1701
+ if (rows.length > limit)
1702
+ throw new ValidationError('peer History exceeds limit');
1703
+ const messages = [];
1704
+ for (const row of rows) {
1705
+ if (String(row.sender_aid ?? row.from_aid ?? row.from ?? '') !== sender)
1706
+ throw new ValidationError('peer History sender mismatch');
1707
+ const plaintext = await this.decodeP2PWindowMessage(row, false, true, 'history');
1708
+ this.ensureDeliveryCurrent(ns, generation);
1709
+ if (plaintext)
1710
+ messages.push({ ...plaintext, need_render: typeof row.need_render === 'boolean' ? row.need_render : plaintext.need_render !== false });
1711
+ }
1712
+ return { ...result, messages };
1713
+ }
1271
1714
  async pullV2TailInternal(params, deferPublish = false) {
1272
1715
  const client = this.client;
1273
1716
  const generation = Number(client._v2RuntimeGeneration ?? 0);
@@ -1278,28 +1721,47 @@ export class V2E2EECoordinator {
1278
1721
  const afterSeq = strictWindowSeq(params.after_seq ?? 0, 'after_seq');
1279
1722
  const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
1280
1723
  const ns = client._aid ? `p2p:${client._aid}` : '';
1724
+ const volatileCursor = String(params.after_volatile_cursor ?? client._delivery.volatilePullCursor?.(ns) ?? '');
1281
1725
  if (ns)
1282
1726
  client._delivery.syncStarted(ns);
1283
- const pullGateKey = pullGateKeyForClient(client, 'message.v2.pull', {
1284
- window_mode: 'tail', after_seq: afterSeq, limit,
1285
- }, `${ns}|tail|after=${afterSeq}|limit=${limit}`);
1286
- const result = await client._callRawV2Rpc('message.v2.pull', { window_mode: 'tail', after_seq: afterSeq, limit, _rpc_foreground: true }, pullGateKey);
1287
- const page = validateTailPage(result, afterSeq, limit);
1288
- const decoded = [];
1289
- for (const msg of page.messages) {
1727
+ const request = (cursor) => {
1728
+ const tailParams = { window_mode: 'tail', after_seq: afterSeq, limit,
1729
+ ...(cursor ? { after_volatile_cursor: cursor } : {}) };
1730
+ const pullGateKey = pullGateKeyForClient(client, 'message.v2.pull', tailParams, `${ns}|tail|after=${afterSeq}|limit=${limit}${cursor ? `|volatile=${cursor}` : ''}`);
1731
+ return client._callRawV2Rpc('message.v2.pull', { ...tailParams, _rpc_foreground: true }, pullGateKey);
1732
+ };
1733
+ let result;
1734
+ try {
1735
+ result = await request(volatileCursor);
1736
+ }
1737
+ catch (error) {
1290
1738
  this.ensureDeliveryCurrent(ns, generation);
1291
- const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail', deferPublish);
1292
- if (plaintext)
1293
- decoded.push(plaintext);
1739
+ if (!volatileCursor || !String(error instanceof Error ? error.message : error).toLowerCase().includes('volatile cursor epoch mismatch'))
1740
+ throw error;
1741
+ client._delivery.resetVolatilePullCursor?.();
1742
+ result = await request('');
1294
1743
  }
1744
+ if (Array.isArray(result?.messages) && result.messages.some((row) => row?.storage === 'volatile'))
1745
+ throw new ValidationError('Tail cannot return volatile messages');
1746
+ const page = validateTailPage(result, afterSeq, limit);
1747
+ const decoded = [];
1748
+ await this.withApplicationPage(ns, 'tail', async () => {
1749
+ for (const msg of page.messages) {
1750
+ this.ensureDeliveryCurrent(ns, generation);
1751
+ const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail', deferPublish);
1752
+ if (plaintext)
1753
+ decoded.push(plaintext);
1754
+ }
1755
+ });
1295
1756
  this.ensureDeliveryCurrent(ns, generation);
1296
1757
  if (ns) {
1297
- client._seqTracker.commitTailWindow(ns, {
1298
- windowStartSeq: page.windowStart,
1299
- headSeq: page.head,
1300
- coveredThroughSeq: page.head,
1301
- pageLimit: limit,
1302
- });
1758
+ if (page.head > afterSeq)
1759
+ client._seqTracker.commitTailWindow(ns, {
1760
+ windowStartSeq: page.windowStart,
1761
+ headSeq: page.head,
1762
+ coveredThroughSeq: page.head,
1763
+ pageLimit: limit,
1764
+ });
1303
1765
  client._persistSeq(ns);
1304
1766
  await client._saveSeqTrackerState?.();
1305
1767
  this.ensureDeliveryCurrent(ns, generation);
@@ -1522,10 +1984,6 @@ export class V2E2EECoordinator {
1522
1984
  pipelineRunning = false;
1523
1985
  pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
1524
1986
  }
1525
- if (volatileMessages.length > 0) {
1526
- decrypted.push(...await this.publishV2VolatileMessages(volatileMessages, 'pull', true));
1527
- volatileCursor = String(client._delivery.volatilePullCursor?.() ?? '').trim();
1528
- }
1529
1987
  const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
1530
1988
  const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1531
1989
  const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
@@ -1556,116 +2014,119 @@ export class V2E2EECoordinator {
1556
2014
  startPipeline(pageMaxSeq, volatileCursor);
1557
2015
  const deferredKeyFetches = new Map();
1558
2016
  let blockedSeq = 0;
1559
- const pageDecryptedBefore = decrypted.length;
1560
- for (const msg of messages) {
1561
- const seq = Number(msg.seq ?? 0);
1562
- if (!Number.isFinite(seq) || seq <= 0)
1563
- continue;
1564
- const version = String(msg.version ?? 'v2');
1565
- if (version === 'v1') {
1566
- const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
1567
- const legacyPayload = legacy.payload !== undefined ? legacy.payload : msg.payload;
1568
- const payloadType = isJsonObject(legacyPayload)
1569
- ? String(legacyPayload.type ?? '').trim()
1570
- : '';
1571
- if (legacyPayload !== undefined && legacyPayload !== null
1572
- && !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
1573
- const v1Msg = {
1574
- message_id: String(msg.message_id ?? ''),
1575
- from: String(msg.from_aid ?? ''),
1576
- to: String(legacy.to ?? client._aid ?? ''),
1577
- seq: msg.seq,
1578
- type: String(msg.type ?? ''),
1579
- timestamp: msg.t_server,
1580
- payload: legacyPayload,
1581
- encrypted: false,
1582
- };
1583
- const direction = String(msg.direction ?? '').trim();
1584
- if (direction)
1585
- v1Msg.direction = direction;
1586
- else if (String(msg.from_aid ?? '').trim() && String(msg.from_aid ?? '').trim() !== String(client._aid ?? '').trim()) {
1587
- v1Msg.direction = 'inbound';
2017
+ let pageDecryptedBefore = decrypted.length;
2018
+ const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
2019
+ const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
2020
+ const serverAckSeq = parsedServerAckSeq ?? 0;
2021
+ const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
2022
+ let commitTarget = pageContigBefore;
2023
+ await this.withApplicationPage(ns, 'pull', async () => {
2024
+ if (volatileMessages.length > 0) {
2025
+ decrypted.push(...await this.publishV2VolatileMessages(volatileMessages, 'pull', true));
2026
+ pageDecryptedBefore = decrypted.length;
2027
+ }
2028
+ for (const msg of messages) {
2029
+ const seq = Number(msg.seq ?? 0);
2030
+ if (!Number.isFinite(seq) || seq <= 0)
2031
+ continue;
2032
+ const version = String(msg.version ?? 'v2');
2033
+ if (version === 'v1') {
2034
+ const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
2035
+ const legacyPayload = legacy.payload !== undefined ? legacy.payload : msg.payload;
2036
+ const payloadType = isJsonObject(legacyPayload)
2037
+ ? String(legacyPayload.type ?? '').trim()
2038
+ : '';
2039
+ if (legacyPayload !== undefined && legacyPayload !== null
2040
+ && !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
2041
+ const v1Msg = {
2042
+ message_id: String(msg.message_id ?? ''),
2043
+ from: String(msg.from_aid ?? ''),
2044
+ to: String(legacy.to ?? client._aid ?? ''),
2045
+ seq: msg.seq,
2046
+ type: String(msg.type ?? ''),
2047
+ timestamp: msg.t_server,
2048
+ payload: legacyPayload,
2049
+ encrypted: false,
2050
+ };
2051
+ const direction = String(msg.direction ?? '').trim();
2052
+ if (direction)
2053
+ v1Msg.direction = direction;
2054
+ else if (String(msg.from_aid ?? '').trim() && String(msg.from_aid ?? '').trim() !== String(client._aid ?? '').trim()) {
2055
+ v1Msg.direction = 'inbound';
2056
+ }
2057
+ if (msg.sender_device_id !== undefined)
2058
+ v1Msg.sender_device_id = msg.sender_device_id;
2059
+ attachGatewayProximity(v1Msg, msg);
2060
+ const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
2061
+ if (ns) {
2062
+ await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
2063
+ }
2064
+ else {
2065
+ await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull');
2066
+ }
2067
+ decrypted.push(v1Msg);
2068
+ client._clientLog.debug(`message.v2.pull plaintext V1 delivered: seq=${seq}, ns=${ns || '<none>'}`);
1588
2069
  }
1589
- if (msg.sender_device_id !== undefined)
1590
- v1Msg.sender_device_id = msg.sender_device_id;
1591
- attachGatewayProximity(v1Msg, msg);
1592
- const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
1593
- if (ns) {
1594
- await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
2070
+ else if (isJsonObject(legacyPayload)
2071
+ && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
2072
+ const deferStatus = {};
2073
+ const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(legacyPayload) }, true, true, true, true, deferStatus, true, 'pull');
2074
+ if (deferStatus.deferred) {
2075
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2076
+ if (deferStatus.fromAid) {
2077
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2078
+ deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
2079
+ }
2080
+ }
2081
+ if (plaintext) {
2082
+ if (ns)
2083
+ await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
2084
+ else
2085
+ await client._publishAppEvent('message.received', plaintext, 'pull');
2086
+ decrypted.push(plaintext);
2087
+ }
1595
2088
  }
1596
2089
  else {
1597
- await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull');
2090
+ client._clientLog.debug(`message.v2.pull skipping V1 envelope seq=${seq} payload_type=${payloadType || '<none>'} (V1 E2EE removed)`);
1598
2091
  }
1599
- decrypted.push(v1Msg);
1600
- client._clientLog.debug(`message.v2.pull plaintext V1 delivered: seq=${seq}, ns=${ns || '<none>'}`);
2092
+ continue;
1601
2093
  }
1602
- else if (isJsonObject(legacyPayload)
1603
- && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1604
- const deferStatus = {};
1605
- const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(legacyPayload) }, true, true, true, true, deferStatus, true, 'pull');
1606
- if (deferStatus.deferred) {
1607
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1608
- if (deferStatus.fromAid) {
1609
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1610
- deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
1611
- }
1612
- }
1613
- if (plaintext) {
1614
- if (ns)
1615
- await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1616
- else
1617
- await client._publishAppEvent('message.received', plaintext, 'pull');
1618
- decrypted.push(plaintext);
1619
- }
2094
+ if (version !== 'v2') {
2095
+ client._clientLog.debug(`message.v2.pull skipping non-V2 row seq=${seq} version=${String(msg.version ?? '')}`);
2096
+ continue;
2097
+ }
2098
+ const spkId = String(msg.spk_id ?? '');
2099
+ if (spkId && client._v2Session && !client._v2Session.isCurrentSPK(spkId)) {
2100
+ client._v2Session.trackOldSPKMaxSeq(spkId, seq);
2101
+ }
2102
+ const deferStatus = {};
2103
+ const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
2104
+ if (deferStatus.deferred) {
2105
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2106
+ }
2107
+ if (deferStatus.deferred && deferStatus.fromAid) {
2108
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2109
+ deferredKeyFetches.set(key, {
2110
+ fromAid: deferStatus.fromAid,
2111
+ senderDeviceId: deferStatus.senderDeviceId ?? '',
2112
+ groupId: deferStatus.groupId ?? '',
2113
+ });
2114
+ }
2115
+ if (plaintext === null) {
2116
+ client._clientLog.debug(`message.v2.pull decrypt returned null: seq=${seq}, ns=${ns || '<none>'}`);
2117
+ continue;
2118
+ }
2119
+ if (ns) {
2120
+ await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1620
2121
  }
1621
2122
  else {
1622
- client._clientLog.debug(`message.v2.pull skipping V1 envelope seq=${seq} payload_type=${payloadType || '<none>'} (V1 E2EE removed)`);
2123
+ await client._publishAppEvent('message.received', plaintext, 'pull');
1623
2124
  }
1624
- continue;
1625
- }
1626
- if (version !== 'v2') {
1627
- client._clientLog.debug(`message.v2.pull skipping non-V2 row seq=${seq} version=${String(msg.version ?? '')}`);
1628
- continue;
1629
- }
1630
- const spkId = String(msg.spk_id ?? '');
1631
- if (spkId && client._v2Session && !client._v2Session.isCurrentSPK(spkId)) {
1632
- client._v2Session.trackOldSPKMaxSeq(spkId, seq);
1633
- }
1634
- const deferStatus = {};
1635
- const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
1636
- if (deferStatus.deferred) {
1637
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1638
- }
1639
- if (deferStatus.deferred && deferStatus.fromAid) {
1640
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1641
- deferredKeyFetches.set(key, {
1642
- fromAid: deferStatus.fromAid,
1643
- senderDeviceId: deferStatus.senderDeviceId ?? '',
1644
- groupId: deferStatus.groupId ?? '',
1645
- });
1646
- }
1647
- if (plaintext === null) {
1648
- client._clientLog.debug(`message.v2.pull decrypt returned null: seq=${seq}, ns=${ns || '<none>'}`);
1649
- continue;
2125
+ decrypted.push(plaintext);
2126
+ client._logMessageDebug('decrypt-ok', 'message.v2.pull', 'message.received', plaintext);
1650
2127
  }
1651
2128
  if (ns) {
1652
- await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1653
- }
1654
- else {
1655
- await client._publishAppEvent('message.received', plaintext, 'pull');
1656
- }
1657
- decrypted.push(plaintext);
1658
- client._logMessageDebug('decrypt-ok', 'message.v2.pull', 'message.received', plaintext);
1659
- }
1660
- const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
1661
- const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
1662
- const serverAckSeq = parsedServerAckSeq ?? 0;
1663
- const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
1664
- if (ns) {
1665
- const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, serverAckSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
1666
- let ackSeq = pageContigBefore;
1667
- let contigAdvanced = false;
1668
- try {
2129
+ commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, serverAckSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
1669
2130
  this.ensureDeliveryCurrent(ns, generation);
1670
2131
  const window = client._seqTracker.getSyncState?.(ns);
1671
2132
  const deliveryTarget = window && commitTarget >= window.tail - 1
@@ -1674,6 +2135,15 @@ export class V2E2EECoordinator {
1674
2135
  await client._drainOrderedMessages(ns, undefined, false, false, 'pull', deliveryTarget);
1675
2136
  }
1676
2137
  this.ensureDeliveryCurrent(ns, generation);
2138
+ }
2139
+ });
2140
+ if (volatileMessages.length > 0)
2141
+ volatileCursor = String(client._delivery.volatilePullCursor?.() ?? '').trim();
2142
+ if (ns) {
2143
+ let ackSeq = pageContigBefore;
2144
+ let contigAdvanced = false;
2145
+ try {
2146
+ this.ensureDeliveryCurrent(ns, generation);
1677
2147
  pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
1678
2148
  if (commitTarget > pageContigBefore) {
1679
2149
  trackerMutated = true;
@@ -1824,11 +2294,27 @@ export class V2E2EECoordinator {
1824
2294
  const client = this.client;
1825
2295
  if (!client._v2Session)
1826
2296
  return [];
2297
+ const generation = Number(client._v2RuntimeGeneration ?? 0);
2298
+ const context = JSON.stringify([client._aid, client._deviceId, client._slotId]);
2299
+ const ensureCurrent = () => {
2300
+ if (!client._p2pPeerPullEnabled)
2301
+ return;
2302
+ if (context !== JSON.stringify([client._aid, client._deviceId, client._slotId]))
2303
+ throw new StateError('delivery invalidated');
2304
+ const sender = String(messages[0]?.sender_aid ?? messages[0]?.from_aid ?? messages[0]?.from ?? '').trim();
2305
+ if (sender)
2306
+ this.ensureDeliveryCurrent(client._delivery.peerNamespace(sender), generation);
2307
+ };
1827
2308
  const processed = [];
1828
2309
  const plaintextMessages = [];
1829
2310
  let complete = true;
1830
2311
  for (const rawMessage of messages) {
1831
- const claim = client._delivery.beginVolatileDelivery(rawMessage);
2312
+ let claim = client._delivery.beginVolatileDelivery(rawMessage);
2313
+ if (claim === 'inflight' && typeof client._delivery.waitVolatileDelivery === 'function') {
2314
+ await client._delivery.waitVolatileDelivery(rawMessage);
2315
+ ensureCurrent();
2316
+ claim = client._delivery.beginVolatileDelivery(rawMessage);
2317
+ }
1832
2318
  if (claim === 'seen') {
1833
2319
  processed.push(rawMessage);
1834
2320
  continue;
@@ -1867,13 +2353,16 @@ export class V2E2EECoordinator {
1867
2353
  complete = false;
1868
2354
  continue;
1869
2355
  }
2356
+ ensureCurrent();
1870
2357
  const publicMessage = client._delivery.publicVolatileMessage(plaintext);
1871
2358
  for (const key of ['storage', 'volatile_cursor', 'created_at', 'expires_at', 'persist']) {
1872
2359
  if (Object.prototype.hasOwnProperty.call(rawMessage, key))
1873
2360
  publicMessage[key] = rawMessage[key];
1874
2361
  }
2362
+ publicMessage.need_render = typeof rawMessage.need_render === 'boolean' ? rawMessage.need_render : publicMessage.need_render !== false;
1875
2363
  const appEvent = client._delivery.p2pAppEventForMessage(publicMessage);
1876
2364
  await client._publishAppEvent(appEvent.event, appEvent.payload, source);
2365
+ ensureCurrent();
1877
2366
  client._delivery.markVolatileSeen(rawMessage);
1878
2367
  processed.push(rawMessage);
1879
2368
  plaintextMessages.push(publicMessage);
@@ -1883,6 +2372,7 @@ export class V2E2EECoordinator {
1883
2372
  }
1884
2373
  }
1885
2374
  if (processed.length > 0) {
2375
+ ensureCurrent();
1886
2376
  await client._delivery.ackVolatileMessages(processed, true, advanceCursor && complete && processed.length === messages.length);
1887
2377
  }
1888
2378
  return plaintextMessages;
@@ -1997,6 +2487,7 @@ export class V2E2EECoordinator {
1997
2487
  timestamp: opts?.timestamp,
1998
2488
  protectedHeaders: opts?.protectedHeaders,
1999
2489
  context: opts?.context,
2490
+ needRender: opts?.needRender,
2000
2491
  useCache,
2001
2492
  });
2002
2493
  const result = await client.call('group.v2.send', withExplicitGroupAid({
@@ -2013,6 +2504,7 @@ export class V2E2EECoordinator {
2013
2504
  payload,
2014
2505
  message_id: messageId,
2015
2506
  timestamp: opts?.timestamp,
2507
+ need_render: opts?.needRender ?? true,
2016
2508
  protected_headers: opts?.protectedHeaders,
2017
2509
  context: opts?.context,
2018
2510
  }, groupAid), result, true);
@@ -2028,6 +2520,7 @@ export class V2E2EECoordinator {
2028
2520
  payload,
2029
2521
  message_id: messageId,
2030
2522
  timestamp: opts?.timestamp,
2523
+ need_render: opts?.needRender ?? true,
2031
2524
  protected_headers: opts?.protectedHeaders,
2032
2525
  context: opts?.context,
2033
2526
  }, groupAid), result, true);
@@ -2152,6 +2645,7 @@ export class V2E2EECoordinator {
2152
2645
  timestamp: opts.timestamp,
2153
2646
  protectedHeaders: opts.protectedHeaders,
2154
2647
  context: opts.context,
2648
+ needRender: opts.needRender,
2155
2649
  }, stateCommitment);
2156
2650
  client._logMessageDebug('send-envelope', 'group.send.v2', 'group.send', {
2157
2651
  group_id: groupId,
@@ -2173,7 +2667,7 @@ export class V2E2EECoordinator {
2173
2667
  });
2174
2668
  return envelope;
2175
2669
  }
2176
- async pullGroupV2TailInternal(params, deferPublish = false) {
2670
+ async pullGroupV2TailInternal(params, deferPublish = false, namespaceLocked = false) {
2177
2671
  const client = this.client;
2178
2672
  const generation = Number(client._v2RuntimeGeneration ?? 0);
2179
2673
  if (params.window_mode !== 'tail')
@@ -2187,6 +2681,7 @@ export class V2E2EECoordinator {
2187
2681
  const afterSeq = strictWindowSeq(params.after_seq ?? 0, 'after_seq');
2188
2682
  const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
2189
2683
  const ns = `group:${groupId}`;
2684
+ const namespaceGeneration = client._delivery.captureSeqNamespaceGeneration?.(ns);
2190
2685
  const explicitCursorParams = isJsonObject(params._group_cursor_params)
2191
2686
  ? params._group_cursor_params : {};
2192
2687
  const cursorParams = Object.keys(explicitCursorParams).length > 0 ? explicitCursorParams : params;
@@ -2203,35 +2698,56 @@ export class V2E2EECoordinator {
2203
2698
  limit,
2204
2699
  _group_cursor_params: explicitCursorParams,
2205
2700
  }, `${ns}|tail|after=${afterSeq}|limit=${limit}`);
2206
- const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2701
+ if (!namespaceLocked)
2702
+ return await client._rpcPipeline.runPullNamespaceSerialized(pullGateKey, () => this.pullGroupV2TailInternal(params, deferPublish, true), false);
2703
+ const result = await client._runPullSerialized(pullGateKey, () => client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2207
2704
  group_id: groupId,
2208
2705
  window_mode: 'tail',
2209
2706
  after_seq: afterSeq,
2210
2707
  limit,
2211
2708
  _group_cursor_params: explicitCursorParams,
2212
2709
  _rpc_foreground: true,
2213
- }, groupAid), pullGateKey);
2710
+ }, groupAid), pullGateKey), false);
2214
2711
  const resultAid = String(result.group_aid ?? result.groupAid ?? '').trim();
2215
2712
  if (resultAid)
2216
2713
  groupAid = resultAid;
2217
2714
  const page = validateTailPage(result, afterSeq, limit);
2218
2715
  const decoded = [];
2219
- for (const msg of page.messages) {
2220
- this.ensureDeliveryCurrent(ns, generation);
2221
- client._logMessageDebug('tail-raw', 'group.v2.pull', 'group.message_created', msg);
2222
- const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, true, false, 'tail', deferPublish);
2223
- if (plaintext)
2224
- decoded.push(plaintext);
2225
- }
2226
- this.ensureDeliveryCurrent(ns, generation);
2227
- client._seqTracker.commitTailWindow(ns, {
2228
- windowStartSeq: page.windowStart,
2229
- headSeq: page.head,
2230
- coveredThroughSeq: page.head,
2231
- pageLimit: limit,
2716
+ await this.withApplicationPage(ns, 'tail', async () => {
2717
+ for (const msg of page.messages) {
2718
+ this.ensureDeliveryCurrent(ns, generation);
2719
+ client._logMessageDebug('tail-raw', 'group.v2.pull', 'group.message_created', msg);
2720
+ const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, true, false, 'tail', deferPublish);
2721
+ if (plaintext)
2722
+ decoded.push(plaintext);
2723
+ }
2232
2724
  });
2233
- client._persistSeq(ns);
2234
- await client._saveSeqTrackerState?.();
2725
+ this.ensureDeliveryCurrent(ns, generation);
2726
+ if (client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) === false)
2727
+ throw new Error('pull invalidated');
2728
+ client._delivery.rememberCommittedSeqState?.(ns);
2729
+ const snapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
2730
+ try {
2731
+ if (page.head > afterSeq)
2732
+ client._seqTracker.commitTailWindow(ns, {
2733
+ windowStartSeq: page.windowStart,
2734
+ headSeq: page.head,
2735
+ coveredThroughSeq: page.head,
2736
+ pageLimit: limit,
2737
+ });
2738
+ await client._commitSeqTrackerState(ns);
2739
+ if (client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) === false)
2740
+ throw new Error('pull invalidated');
2741
+ }
2742
+ catch (error) {
2743
+ if (Number(client._v2RuntimeGeneration ?? 0) === generation
2744
+ && client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) !== false
2745
+ && client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false) {
2746
+ (client._seqTracker._restoreNamespaceSnapshotPreservingPush ?? client._seqTracker.restoreNamespaceSnapshot)
2747
+ .call(client._seqTracker, ns, snapshot);
2748
+ }
2749
+ throw error;
2750
+ }
2235
2751
  this.ensureDeliveryCurrent(ns, generation);
2236
2752
  const ack = client._seqTracker.getContiguousSeq(ns);
2237
2753
  if (ownsCursor)
@@ -2253,12 +2769,13 @@ export class V2E2EECoordinator {
2253
2769
  let groupAid = String(params.group_aid ?? '').trim();
2254
2770
  const beforeSeq = strictWindowSeq(params.before_seq, 'before_seq', true);
2255
2771
  const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
2256
- const result = await client._callRawV2Rpc('group.history', withExplicitGroupAid({
2772
+ const key = pullGateKeyForClient(client, 'group.history', { group_id: groupId, before_seq: beforeSeq, limit }, `group:${groupId}|history|before=${beforeSeq}|limit=${limit}`);
2773
+ const result = await client._runPullSerialized(key, () => client._callRawV2Rpc('group.history', withExplicitGroupAid({
2257
2774
  group_id: groupId,
2258
2775
  before_seq: beforeSeq,
2259
2776
  limit,
2260
2777
  _rpc_foreground: true,
2261
- }, groupAid));
2778
+ }, groupAid), key), false);
2262
2779
  const resultAid = String(result.group_aid ?? result.groupAid ?? '').trim();
2263
2780
  if (resultAid)
2264
2781
  groupAid = resultAid;
@@ -2295,7 +2812,7 @@ export class V2E2EECoordinator {
2295
2812
  group_id: gid, window_mode: 'tail', after_seq: afterSeq, limit,
2296
2813
  _group_cursor_params: opts?.cursorParams,
2297
2814
  }, `${ns}|tail|after=${afterSeq}|limit=${limit}`);
2298
- return (await client._runPullSerialized(key, () => this.pullGroupV2Impl(gid, afterSeq, limit, {
2815
+ return (await client._rpcPipeline.runPullNamespaceSerialized(key, () => this.pullGroupV2Impl(gid, afterSeq, limit, {
2299
2816
  ...opts,
2300
2817
  gateLocked: true,
2301
2818
  }), false));
@@ -2308,7 +2825,7 @@ export class V2E2EECoordinator {
2308
2825
  window_mode: 'tail',
2309
2826
  after_seq: afterSeq,
2310
2827
  limit,
2311
- });
2828
+ }, false, true);
2312
2829
  return (Array.isArray(result.messages) ? result.messages : []);
2313
2830
  }
2314
2831
  if (!opts?.gateLocked) {
@@ -2316,20 +2833,17 @@ export class V2E2EECoordinator {
2316
2833
  group_id: gid, after_seq: afterSeq, force: opts?.force === true, limit,
2317
2834
  _group_cursor_params: opts?.cursorParams,
2318
2835
  }, `${ns}|forward|after=${afterSeq}|force=${opts?.force === true}|limit=${limit}`);
2319
- return await client._runPullSerialized(key, async () => this.pullGroupV2Impl(gid, afterSeq, limit, {
2836
+ return await client._rpcPipeline.runPullNamespaceSerialized(key, async () => this.pullGroupV2Impl(gid, afterSeq, limit, {
2320
2837
  ...(opts ?? {}),
2321
2838
  gateLocked: true,
2322
2839
  scheduleFollowup: true,
2323
- }), true);
2840
+ }), opts?.background !== false);
2324
2841
  }
2325
2842
  const decrypted = [];
2326
2843
  const wireGroupId = String(opts?.wireGroupId ?? groupId ?? '').trim() || gid;
2327
2844
  const cursorParams = opts?.cursorParams ?? {};
2328
2845
  const ownsCursor = opts?.ownsCursor !== false;
2329
- let pullGateKey = pullGateKeyForClient(client, 'group.v2.pull', {
2330
- group_id: gid, after_seq: afterSeq, force: opts?.force === true, limit,
2331
- _group_cursor_params: cursorParams,
2332
- }, `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
2846
+ const background = opts?.background !== false;
2333
2847
  let nextAfterSeq = (opts?.explicitAfterSeq || opts?.force)
2334
2848
  ? afterSeq
2335
2849
  : (afterSeq || client._seqTracker.getContiguousSeq(ns));
@@ -2340,6 +2854,21 @@ export class V2E2EECoordinator {
2340
2854
  let pageCount = 0;
2341
2855
  let lastAutoAckSeq = 0;
2342
2856
  let pendingAckSeq = deferredForwardAck;
2857
+ let committedAckSeq = ownsCursor ? client._seqTracker.getContiguousSeq(ns) : 0;
2858
+ const fetchPage = async (after, ack) => {
2859
+ const key = pullGateKeyForClient(client, 'group.v2.pull', {
2860
+ group_id: gid, after_seq: after, force: opts?.force === true, limit, _group_cursor_params: cursorParams,
2861
+ }, `${ns}|forward|after=${after}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
2862
+ return await client._runPullSerialized(key, async () => {
2863
+ this.ensureDeliveryCurrent(ns, generation);
2864
+ return await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2865
+ group_id: wireGroupId, after_seq: after, limit, ...cursorParams,
2866
+ ...(opts?.force ? { force: true } : {}),
2867
+ ...(ack > 0 ? { ack_up_to_seq: Math.min(ack, committedAckSeq) } : {}),
2868
+ _rpc_background: background, _rpc_foreground: !background,
2869
+ }, groupAid), key);
2870
+ }, background);
2871
+ };
2343
2872
  const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
2344
2873
  let pipelineRunning = false;
2345
2874
  let pipelineError = null;
@@ -2356,16 +2885,9 @@ export class V2E2EECoordinator {
2356
2885
  if (pipelineStopped)
2357
2886
  break;
2358
2887
  this.ensureDeliveryCurrent(ns, generation);
2359
- const ackUpToSeq = pendingAckSeq;
2888
+ const ackUpToSeq = Math.min(pendingAckSeq, committedAckSeq);
2360
2889
  const inlineAckGeneration = ackUpToSeq > 0 ? client._delivery.captureGroupInlineAckGeneration() : null;
2361
- const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2362
- group_id: wireGroupId,
2363
- after_seq: producerAfterSeq,
2364
- limit,
2365
- ...cursorParams,
2366
- ...(opts?.force ? { force: true } : {}),
2367
- ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
2368
- }, groupAid), pullGateKey);
2890
+ const result = await fetchPage(producerAfterSeq, ackUpToSeq);
2369
2891
  if (pipelineStopped)
2370
2892
  break;
2371
2893
  this.ensureDeliveryCurrent(ns, generation);
@@ -2442,14 +2964,7 @@ export class V2E2EECoordinator {
2442
2964
  result = pipelinePage.result;
2443
2965
  }
2444
2966
  else {
2445
- result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2446
- group_id: wireGroupId,
2447
- after_seq: nextAfterSeq,
2448
- limit,
2449
- ...cursorParams,
2450
- ...(opts?.force ? { force: true } : {}),
2451
- ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
2452
- }, groupAid), pullGateKey);
2967
+ result = await fetchPage(nextAfterSeq, ackUpToSeq);
2453
2968
  }
2454
2969
  if (ownsCursor)
2455
2970
  this.ensureDeliveryCurrent(ns, generation);
@@ -2486,6 +3001,7 @@ export class V2E2EECoordinator {
2486
3001
  .map((msg) => Number(msg.seq ?? 0))
2487
3002
  .filter((seq) => Number.isFinite(seq) && seq > 0);
2488
3003
  const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
3004
+ const namespaceGeneration = client._delivery.captureSeqNamespaceGeneration?.(ns);
2489
3005
  let pageTrackerSnapshot = null;
2490
3006
  let trackerMutated = false;
2491
3007
  let pageMaxSeq = nextAfterSeq;
@@ -2503,30 +3019,66 @@ export class V2E2EECoordinator {
2503
3019
  const deferredKeyFetches = new Map();
2504
3020
  let blockedSeq = 0;
2505
3021
  const pageDecryptedBefore = decrypted.length;
2506
- for (const msg of messages) {
2507
- const seq = Number(msg.seq ?? 0);
2508
- if (!Number.isFinite(seq) || seq <= 0)
2509
- continue;
2510
- const version = String(msg.version ?? 'v2');
2511
- if (version === 'v1') {
2512
- const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
2513
- const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
2514
- const payloadObj = isJsonObject(payload) ? payload : null;
2515
- const payloadType = payloadObj ? String(payloadObj.type ?? '').trim() : '';
2516
- // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
2517
- if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
2518
- && client._delivery.recallEventFromGroupMessage(msg)) {
2519
- await client._delivery.publishGroupRecallTombstone(gid, seq, {
2520
- ...msg,
2521
- group_id: gid,
2522
- group_aid: eventGroupAid,
2523
- }, 'pull');
2524
- client._markPublishedSeq(ns, seq);
2525
- client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
3022
+ const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
3023
+ const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
3024
+ const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
3025
+ const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq'));
3026
+ const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
3027
+ const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
3028
+ let commitTarget = pageContigBefore;
3029
+ await this.withApplicationPage(ns, 'pull', async () => {
3030
+ for (const msg of messages) {
3031
+ const seq = Number(msg.seq ?? 0);
3032
+ if (!Number.isFinite(seq) || seq <= 0)
2526
3033
  continue;
2527
- }
2528
- if (payloadObj) {
2529
- if (!['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
3034
+ const version = String(msg.version ?? 'v2');
3035
+ if (version === 'v1') {
3036
+ const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
3037
+ const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
3038
+ const payloadObj = isJsonObject(payload) ? payload : null;
3039
+ const payloadType = payloadObj ? String(payloadObj.type ?? '').trim() : '';
3040
+ // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
3041
+ if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
3042
+ && client._delivery.recallEventFromGroupMessage(msg)) {
3043
+ await client._delivery.publishGroupRecallTombstone(gid, seq, {
3044
+ ...msg,
3045
+ group_id: gid,
3046
+ group_aid: eventGroupAid,
3047
+ }, 'pull');
3048
+ client._markPublishedSeq(ns, seq);
3049
+ client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
3050
+ continue;
3051
+ }
3052
+ if (payloadObj) {
3053
+ if (!['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
3054
+ let v1Msg = {
3055
+ message_id: String(msg.message_id ?? ''),
3056
+ from: String(msg.from_aid ?? ''),
3057
+ group_id: gid,
3058
+ group_aid: eventGroupAid,
3059
+ seq: msg.seq,
3060
+ type: String(msg.type ?? ''),
3061
+ timestamp: msg.t_server,
3062
+ payload,
3063
+ encrypted: false,
3064
+ need_render: msg.need_render !== false,
3065
+ };
3066
+ if (msg.sender_device_id !== undefined)
3067
+ v1Msg.sender_device_id = msg.sender_device_id;
3068
+ v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
3069
+ attachGatewayProximity(v1Msg, msg);
3070
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
3071
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
3072
+ }
3073
+ else {
3074
+ client._markPublishedSeq(ns, seq);
3075
+ }
3076
+ decrypted.push(v1Msg);
3077
+ client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
3078
+ continue;
3079
+ }
3080
+ }
3081
+ else if (payload !== undefined && payload !== null) {
2530
3082
  let v1Msg = {
2531
3083
  message_id: String(msg.message_id ?? ''),
2532
3084
  from: String(msg.from_aid ?? ''),
@@ -2537,114 +3089,81 @@ export class V2E2EECoordinator {
2537
3089
  timestamp: msg.t_server,
2538
3090
  payload,
2539
3091
  encrypted: false,
3092
+ need_render: msg.need_render !== false,
2540
3093
  };
2541
3094
  if (msg.sender_device_id !== undefined)
2542
3095
  v1Msg.sender_device_id = msg.sender_device_id;
2543
3096
  v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
2544
3097
  attachGatewayProximity(v1Msg, msg);
2545
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2546
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2547
- }
2548
- else {
2549
- client._markPublishedSeq(ns, seq);
2550
- }
3098
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2551
3099
  decrypted.push(v1Msg);
2552
3100
  client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
2553
3101
  continue;
2554
3102
  }
2555
- }
2556
- else if (payload !== undefined && payload !== null) {
2557
- let v1Msg = {
2558
- message_id: String(msg.message_id ?? ''),
2559
- from: String(msg.from_aid ?? ''),
2560
- group_id: gid,
2561
- group_aid: eventGroupAid,
2562
- seq: msg.seq,
2563
- type: String(msg.type ?? ''),
2564
- timestamp: msg.t_server,
2565
- payload,
2566
- encrypted: false,
2567
- };
2568
- if (msg.sender_device_id !== undefined)
2569
- v1Msg.sender_device_id = msg.sender_device_id;
2570
- v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
2571
- attachGatewayProximity(v1Msg, msg);
2572
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2573
- decrypted.push(v1Msg);
2574
- client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
2575
- continue;
2576
- }
2577
- if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
2578
- const deferStatus = {};
2579
- const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
2580
- if (deferStatus.deferred) {
2581
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2582
- if (deferStatus.fromAid) {
2583
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2584
- deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
3103
+ if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
3104
+ const deferStatus = {};
3105
+ const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
3106
+ if (deferStatus.deferred) {
3107
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
3108
+ if (deferStatus.fromAid) {
3109
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
3110
+ deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
3111
+ }
2585
3112
  }
2586
- }
2587
- if (plaintext) {
2588
- plaintext.group_id = gid;
2589
- plaintext.group_aid = eventGroupAid;
2590
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2591
- await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2592
- }
2593
- else {
2594
- client._markPublishedSeq(ns, seq);
3113
+ if (plaintext) {
3114
+ plaintext.group_id = gid;
3115
+ plaintext.group_aid = eventGroupAid;
3116
+ plaintext.need_render = typeof msg.need_render === 'boolean' ? msg.need_render : plaintext.need_render !== false;
3117
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
3118
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
3119
+ }
3120
+ else {
3121
+ client._markPublishedSeq(ns, seq);
3122
+ }
3123
+ decrypted.push(plaintext);
2595
3124
  }
2596
- decrypted.push(plaintext);
2597
3125
  }
3126
+ else {
3127
+ client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
3128
+ }
3129
+ continue;
3130
+ }
3131
+ if (version !== 'v2') {
3132
+ client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
3133
+ continue;
3134
+ }
3135
+ const deferStatus = {};
3136
+ let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
3137
+ if (deferStatus.deferred) {
3138
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
3139
+ }
3140
+ if (deferStatus.deferred && deferStatus.fromAid) {
3141
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
3142
+ deferredKeyFetches.set(key, {
3143
+ fromAid: deferStatus.fromAid,
3144
+ senderDeviceId: deferStatus.senderDeviceId ?? '',
3145
+ groupId: deferStatus.groupId ?? '',
3146
+ });
3147
+ }
3148
+ if (plaintext === null) {
3149
+ client._clientLog.debug(`group.v2.pull decrypt returned null: group=${gid}, seq=${seq}`);
3150
+ continue;
3151
+ }
3152
+ plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
3153
+ plaintext.group_id = gid;
3154
+ plaintext.group_aid = eventGroupAid;
3155
+ plaintext.need_render = typeof msg.need_render === 'boolean' ? msg.need_render : plaintext.need_render !== false;
3156
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
3157
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2598
3158
  }
2599
3159
  else {
2600
- client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
3160
+ client._markPublishedSeq(ns, seq);
2601
3161
  }
2602
- continue;
2603
- }
2604
- if (version !== 'v2') {
2605
- client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
2606
- continue;
2607
- }
2608
- const deferStatus = {};
2609
- let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
2610
- if (deferStatus.deferred) {
2611
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2612
- }
2613
- if (deferStatus.deferred && deferStatus.fromAid) {
2614
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2615
- deferredKeyFetches.set(key, {
2616
- fromAid: deferStatus.fromAid,
2617
- senderDeviceId: deferStatus.senderDeviceId ?? '',
2618
- groupId: deferStatus.groupId ?? '',
2619
- });
2620
- }
2621
- if (plaintext === null) {
2622
- client._clientLog.debug(`group.v2.pull decrypt returned null: group=${gid}, seq=${seq}`);
2623
- continue;
2624
- }
2625
- plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
2626
- plaintext.group_id = gid;
2627
- plaintext.group_aid = eventGroupAid;
2628
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2629
- await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
3162
+ decrypted.push(plaintext);
3163
+ client._logMessageDebug('decrypt-ok', 'group.v2.pull', 'group.message_created', plaintext);
2630
3164
  }
2631
- else {
2632
- client._markPublishedSeq(ns, seq);
2633
- }
2634
- decrypted.push(plaintext);
2635
- client._logMessageDebug('decrypt-ok', 'group.v2.pull', 'group.message_created', plaintext);
2636
- }
2637
- const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
2638
- const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
2639
- const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
2640
- const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq'));
2641
- const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
2642
- const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
2643
- const commitTarget = Math.max(pageContigBefore, effectiveFloor, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, ownsCursor ? cursorCurrentSeq : 0, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
2644
- let ackSeq = pageContigBefore;
2645
- let contigAdvanced = false;
2646
- if (ownsCursor) {
2647
- try {
3165
+ commitTarget = Math.max(pageContigBefore, effectiveFloor, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, ownsCursor ? cursorCurrentSeq : 0, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
3166
+ if (ownsCursor) {
2648
3167
  this.ensureDeliveryCurrent(ns, generation);
2649
3168
  const window = client._seqTracker.getSyncState?.(ns);
2650
3169
  const deliveryTarget = window && commitTarget >= window.tail - 1
@@ -2653,6 +3172,16 @@ export class V2E2EECoordinator {
2653
3172
  await client._drainOrderedMessages(ns, undefined, false, false, 'pull', deliveryTarget);
2654
3173
  }
2655
3174
  this.ensureDeliveryCurrent(ns, generation);
3175
+ }
3176
+ });
3177
+ let ackSeq = pageContigBefore;
3178
+ let contigAdvanced = false;
3179
+ if (ownsCursor) {
3180
+ try {
3181
+ this.ensureDeliveryCurrent(ns, generation);
3182
+ if (client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) === false)
3183
+ throw new Error('pull invalidated');
3184
+ client._delivery.rememberCommittedSeqState?.(ns);
2656
3185
  pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
2657
3186
  if (commitTarget > pageContigBefore) {
2658
3187
  trackerMutated = true;
@@ -2672,12 +3201,16 @@ export class V2E2EECoordinator {
2672
3201
  if (contigAdvanced) {
2673
3202
  await client._commitSeqTrackerState(ns);
2674
3203
  this.ensureDeliveryCurrent(ns, generation);
3204
+ if (client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) === false)
3205
+ throw new Error('pull invalidated');
2675
3206
  if (pageCount === 1 && deferredServerCursor > 0)
2676
3207
  this.clearForwardCursor(ns, ackSeq);
2677
3208
  }
3209
+ committedAckSeq = ackSeq;
2678
3210
  }
2679
3211
  catch (exc) {
2680
3212
  if (trackerMutated && Number(client._v2RuntimeGeneration ?? 0) === generation
3213
+ && client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) !== false
2681
3214
  && client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false
2682
3215
  && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
2683
3216
  (client._seqTracker._restoreNamespaceSnapshotPreservingPush ?? client._seqTracker.restoreNamespaceSnapshot)
@@ -2749,14 +3282,6 @@ export class V2E2EECoordinator {
2749
3282
  if (shouldContinueForPipeline && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
2750
3283
  startPipeline(nextAfter);
2751
3284
  }
2752
- if (!pipelineRunning && pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
2753
- const nextKey = pullGateKeyForClient(client, 'group.v2.pull', {
2754
- group_id: gid, after_seq: nextAfter, force: opts?.force === true, limit,
2755
- _group_cursor_params: cursorParams,
2756
- }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
2757
- await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
2758
- pullGateKey = nextKey;
2759
- }
2760
3285
  const tailDelayMs = pipelineRunning ? 0 : v2PullTailDelayMs(rawCount, limit);
2761
3286
  if (tailDelayMs > 0)
2762
3287
  await v2Sleep(tailDelayMs);
@@ -2773,7 +3298,7 @@ export class V2E2EECoordinator {
2773
3298
  this.ensureDeliveryCurrent(ns, generation);
2774
3299
  client._clientLog.debug(`group.v2.pull flushing pending auto-ack: group=${gid}, ns=${ns}, ack_seq=${pendingAckSeq}`);
2775
3300
  const ackToFlush = pendingAckSeq;
2776
- await this.confirmForwardGroupAck(gid, ackToFlush, groupAid);
3301
+ await this.confirmForwardGroupAck(gid, ackToFlush, groupAid, background);
2777
3302
  if (pendingAckSeq <= ackToFlush)
2778
3303
  pendingAckSeq = 0;
2779
3304
  }
@@ -2794,11 +3319,11 @@ export class V2E2EECoordinator {
2794
3319
  throw error;
2795
3320
  }
2796
3321
  }
2797
- async confirmForwardGroupAck(groupId, upToSeq, groupAid) {
3322
+ async confirmForwardGroupAck(groupId, upToSeq, groupAid, background = false) {
2798
3323
  const ns = `group:${String(groupId ?? '').trim()}`;
2799
3324
  const generation = Number(this.client._v2RuntimeGeneration ?? 0);
2800
3325
  try {
2801
- const result = await this.ackGroupV2ForForward(groupId, upToSeq, groupAid);
3326
+ const result = await this.ackGroupV2ForForward(groupId, upToSeq, groupAid, background);
2802
3327
  this.ensureDeliveryCurrent(ns, generation);
2803
3328
  const actualAckSeq = this.client._delivery.resolveForwardAckSeq(result, upToSeq);
2804
3329
  if (actualAckSeq < upToSeq) {
@@ -2816,10 +3341,10 @@ export class V2E2EECoordinator {
2816
3341
  async ackGroupV2(groupId, upToSeq, groupAid) {
2817
3342
  return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, false);
2818
3343
  }
2819
- async ackGroupV2ForForward(groupId, upToSeq, groupAid) {
2820
- return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, true);
3344
+ async ackGroupV2ForForward(groupId, upToSeq, groupAid, background = false) {
3345
+ return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, true, background);
2821
3346
  }
2822
- async ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, requireServerWatermark) {
3347
+ async ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, requireServerWatermark, background) {
2823
3348
  const client = this.client;
2824
3349
  const gid = String(groupId ?? '').trim();
2825
3350
  if (!gid)
@@ -2837,7 +3362,9 @@ export class V2E2EECoordinator {
2837
3362
  return { acked: 0 };
2838
3363
  const inlineAckGeneration = client._delivery.captureGroupInlineAckGeneration();
2839
3364
  client._clientLog.debug(`group.v2.ack send: group=${gid}, ns=${ns}, up_to_seq=${seq}`);
2840
- const result = await client._callRawV2Rpc('group.v2.ack', withExplicitGroupAid({ group_id: gid, up_to_seq: seq }, explicitGroupAid));
3365
+ const result = await client._callRawV2Rpc('group.v2.ack', withExplicitGroupAid({ group_id: gid, up_to_seq: seq,
3366
+ ...(background === undefined ? {} : { _rpc_background: background, _rpc_foreground: !background }),
3367
+ }, explicitGroupAid));
2841
3368
  const actualAckSeq = requireServerWatermark
2842
3369
  ? client._delivery.resolveForwardAckSeq(result, seq)
2843
3370
  : client._delivery.resolveActualAckSeq(result, seq);