@agentunion/fastaun-browser 0.5.16 → 0.5.18

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 (83) hide show
  1. package/CHANGELOG.md +334 -258
  2. package/_packed_docs/CHANGELOG.md +334 -258
  3. package/_packed_docs/INDEX.md +36 -5
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +13 -5
  5. package/_packed_docs/agent.md/SCHEMA.md +52 -33
  6. 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 +1170 -1164
  7. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +1215 -1213
  8. package/_packed_docs/protocol/01-/350/272/253/344/273/275/344/270/216/345/207/255/350/257/201/345/215/217/350/256/256-auth.md +95 -6
  9. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +58 -43
  10. package/_packed_docs/protocol/07-/351/224/231/350/257/257/347/240/201/344/270/216/347/212/266/346/200/201/346/234/272.md +19 -4
  11. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +80 -17
  12. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +25 -2
  13. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +69 -10
  14. package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +7 -4
  15. package/_packed_docs/sdk/09-group-rpc-manual.md +167 -35
  16. package/_packed_docs/sdk/09-message-rpc-manual.md +72 -42
  17. package/_packed_docs/sdk/09-storage-rpc-manual.md +48 -18
  18. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +16 -12
  19. package/_packed_docs/sdk/INDEX.md +14 -13
  20. package/dist/agent-md-schema.d.ts.map +1 -1
  21. package/dist/agent-md-schema.js +2 -0
  22. package/dist/agent-md-schema.js.map +1 -1
  23. package/dist/agent-md.d.ts +3 -1
  24. package/dist/agent-md.d.ts.map +1 -1
  25. package/dist/agent-md.js +13 -4
  26. package/dist/agent-md.js.map +1 -1
  27. package/dist/aid-store.d.ts +45 -0
  28. package/dist/aid-store.d.ts.map +1 -1
  29. package/dist/aid-store.js +231 -3
  30. package/dist/aid-store.js.map +1 -1
  31. package/dist/aid.d.ts +17 -1
  32. package/dist/aid.d.ts.map +1 -1
  33. package/dist/aid.js +49 -2
  34. package/dist/aid.js.map +1 -1
  35. package/dist/bundle.js +2763 -1591
  36. package/dist/cert-utils.d.ts.map +1 -1
  37. package/dist/cert-utils.js +5 -3
  38. package/dist/cert-utils.js.map +1 -1
  39. package/dist/client/delivery.d.ts +19 -0
  40. package/dist/client/delivery.d.ts.map +1 -1
  41. package/dist/client/delivery.js +198 -12
  42. package/dist/client/delivery.js.map +1 -1
  43. package/dist/client/group-state.d.ts +4 -0
  44. package/dist/client/group-state.d.ts.map +1 -1
  45. package/dist/client/group-state.js +63 -0
  46. package/dist/client/group-state.js.map +1 -1
  47. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  48. package/dist/client/rpc-pipeline.js +46 -5
  49. package/dist/client/rpc-pipeline.js.map +1 -1
  50. package/dist/client/v2-e2ee.d.ts +7 -1
  51. package/dist/client/v2-e2ee.d.ts.map +1 -1
  52. package/dist/client/v2-e2ee.js +918 -463
  53. package/dist/client/v2-e2ee.js.map +1 -1
  54. package/dist/client.d.ts.map +1 -1
  55. package/dist/client.js +73 -14
  56. package/dist/client.js.map +1 -1
  57. package/dist/facades.d.ts +7 -0
  58. package/dist/facades.d.ts.map +1 -1
  59. package/dist/facades.js +21 -0
  60. package/dist/facades.js.map +1 -1
  61. package/dist/group-fs.d.ts.map +1 -1
  62. package/dist/group-fs.js +8 -5
  63. package/dist/group-fs.js.map +1 -1
  64. package/dist/index.d.ts +3 -2
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +1 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/ownership-proof.d.ts +30 -0
  69. package/dist/ownership-proof.d.ts.map +1 -0
  70. package/dist/ownership-proof.js +97 -0
  71. package/dist/ownership-proof.js.map +1 -0
  72. package/dist/storage/vfs.d.ts +1 -0
  73. package/dist/storage/vfs.d.ts.map +1 -1
  74. package/dist/storage/vfs.js +68 -5
  75. package/dist/storage/vfs.js.map +1 -1
  76. package/dist/tools/cross-sdk-agent.js +34 -3
  77. package/dist/tools/cross-sdk-agent.js.map +1 -1
  78. package/dist/transport.js +34 -34
  79. package/dist/types.d.ts +16 -1
  80. package/dist/types.d.ts.map +1 -1
  81. package/dist/version.d.ts +1 -1
  82. package/dist/version.js +1 -1
  83. package/package.json +13 -13
@@ -14,6 +14,52 @@ const V2_GROUP_STALE_BOOTSTRAP_CODE = -33054;
14
14
  const V2_PULL_DEFAULT_PAGE_LIMIT = 50;
15
15
  const V2_PULL_MAX_PAGE_LIMIT = 50;
16
16
  const V2_SENDER_IK_SYNC_FETCH_TIMEOUT_MS = 3_000;
17
+ const V2_PULL_PIPELINE_QUEUE_CAPACITY = 100;
18
+ class V2PullPageQueue {
19
+ capacity;
20
+ items = [];
21
+ takers = [];
22
+ putters = [];
23
+ queuedWeight = 0;
24
+ closed = false;
25
+ constructor(capacity) {
26
+ this.capacity = capacity;
27
+ }
28
+ async put(item, weight = 1) {
29
+ const itemWeight = Math.max(1, Math.trunc(weight));
30
+ while (!this.closed && this.items.length > 0 && this.queuedWeight + itemWeight > this.capacity) {
31
+ await new Promise((resolve) => this.putters.push(resolve));
32
+ }
33
+ if (this.closed)
34
+ return false;
35
+ const taker = this.takers.shift();
36
+ if (taker)
37
+ taker(item);
38
+ else {
39
+ this.items.push(item);
40
+ this.queuedWeight += itemWeight;
41
+ }
42
+ return true;
43
+ }
44
+ async take() {
45
+ const item = this.items.shift();
46
+ if (item !== undefined) {
47
+ this.queuedWeight = Math.max(0, this.queuedWeight - Math.max(1, Math.trunc(Number(item.weight ?? 1))));
48
+ this.putters.shift()?.();
49
+ return item;
50
+ }
51
+ if (this.closed)
52
+ return undefined;
53
+ return await new Promise((resolve) => this.takers.push(resolve));
54
+ }
55
+ close() {
56
+ this.closed = true;
57
+ for (const resolve of this.takers.splice(0))
58
+ resolve(undefined);
59
+ for (const resolve of this.putters.splice(0))
60
+ resolve();
61
+ }
62
+ }
17
63
  const V2_SESSION_INIT_RETRY_LIMIT = 12;
18
64
  const V2_SESSION_INIT_RETRY_BASE_MS = 1000;
19
65
  const V2_SESSION_INIT_RETRY_MAX_MS = 5000;
@@ -948,6 +994,12 @@ export class V2E2EECoordinator {
948
994
  await client._publishAppEvent(eventName, safeEvent, 'pending_retry');
949
995
  continue;
950
996
  }
997
+ if (!entry.groupId && String(entry.msg.storage ?? '') === 'volatile') {
998
+ await this.publishPendingV2VolatileMessage(entry.msg, plaintext, 'pending_retry');
999
+ client._v2SenderIKPending.delete(key);
1000
+ client._clientLog.debug(`V2 sender IK pending retry delivered: key=${key}`);
1001
+ continue;
1002
+ }
951
1003
  client._v2SenderIKPending.delete(key);
952
1004
  const seq = Number(entry.msg.seq ?? 0);
953
1005
  if (entry.groupId) {
@@ -967,10 +1019,35 @@ export class V2E2EECoordinator {
967
1019
  client._v2SenderIKFetching.delete(fetchKey);
968
1020
  }
969
1021
  }
1022
+ async publishPendingV2VolatileMessage(rawMessage, plaintext, source) {
1023
+ const delivery = this.client._delivery;
1024
+ const claim = delivery.beginVolatileDelivery(rawMessage);
1025
+ if (claim === 'inflight')
1026
+ return;
1027
+ if (claim === 'seen') {
1028
+ await delivery.ackVolatileMessages([rawMessage], true, false);
1029
+ return;
1030
+ }
1031
+ try {
1032
+ const publicMessage = delivery.publicVolatileMessage(plaintext);
1033
+ for (const key of ['storage', 'volatile_cursor', 'created_at', 'expires_at', 'persist']) {
1034
+ if (key in rawMessage)
1035
+ publicMessage[key] = rawMessage[key];
1036
+ }
1037
+ const appEvent = delivery.p2pAppEventForMessage(publicMessage);
1038
+ await this.client._publishAppEvent(appEvent.event, appEvent.payload, source);
1039
+ delivery.markVolatileSeen(rawMessage);
1040
+ }
1041
+ finally {
1042
+ delivery.finishVolatileDelivery(rawMessage);
1043
+ }
1044
+ await delivery.ackVolatileMessages([rawMessage], true, false);
1045
+ }
970
1046
  async sendV2(to, payload, opts) {
971
1047
  const client = this.client;
972
1048
  await client._ensureV2SessionReady('message.send', 'V2 session not initialized; encrypted message.send requires V2 (V1 E2EE removed)');
973
1049
  const toAid = String(to ?? '').trim();
1050
+ const targetDeviceId = String(opts?.deviceId ?? '').trim();
974
1051
  if (!toAid)
975
1052
  throw new ValidationError("message.send requires 'to'");
976
1053
  if (!isJsonObject(payload))
@@ -981,6 +1058,7 @@ export class V2E2EECoordinator {
981
1058
  payload,
982
1059
  messageId: opts?.messageId,
983
1060
  timestamp: opts?.timestamp,
1061
+ deviceId: targetDeviceId || undefined,
984
1062
  protectedHeaders: opts?.protectedHeaders,
985
1063
  context: opts?.context,
986
1064
  useCache,
@@ -989,6 +1067,8 @@ export class V2E2EECoordinator {
989
1067
  to: toAid,
990
1068
  payload: envelope,
991
1069
  message_id: opts?.messageId,
1070
+ ...(opts?.ttl !== undefined ? { ttl: opts.ttl } : {}),
1071
+ ...(targetDeviceId ? { device_id: targetDeviceId } : {}),
992
1072
  encrypt: false,
993
1073
  _skip_send_result_envelope: true,
994
1074
  });
@@ -1046,6 +1126,11 @@ export class V2E2EECoordinator {
1046
1126
  to: String(legacy.to ?? client._aid ?? ''), seq, type: String(msg.type ?? ''),
1047
1127
  timestamp: msg.t_server, payload: payload, encrypted: false,
1048
1128
  };
1129
+ if (msg.sender_device_id !== undefined)
1130
+ plaintext.sender_device_id = msg.sender_device_id;
1131
+ const direction = String(msg.direction ?? '').trim();
1132
+ if (direction)
1133
+ plaintext.direction = direction;
1049
1134
  attachGatewayProximity(plaintext, msg);
1050
1135
  }
1051
1136
  else if (isJsonObject(payload)
@@ -1065,6 +1150,9 @@ export class V2E2EECoordinator {
1065
1150
  client._clientLog.warn(`P2P Tail 单条消息解密失败,连续性证明仍保留: seq=${seq}`);
1066
1151
  return null;
1067
1152
  }
1153
+ if (publish && client._delivery.isSelfSentP2PMessage?.(plaintext)) {
1154
+ return plaintext;
1155
+ }
1068
1156
  if (publish) {
1069
1157
  const event = client._delivery.p2pAppEventForMessage(plaintext);
1070
1158
  if (orderedPublish) {
@@ -1121,6 +1209,8 @@ export class V2E2EECoordinator {
1121
1209
  group_id: groupId, group_aid: groupAid, seq, type: String(msg.type ?? ''),
1122
1210
  timestamp: msg.t_server, payload: payload, encrypted: false,
1123
1211
  };
1212
+ if (msg.sender_device_id !== undefined)
1213
+ plaintext.sender_device_id = msg.sender_device_id;
1124
1214
  plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
1125
1215
  attachGatewayProximity(plaintext, msg);
1126
1216
  }
@@ -1232,6 +1322,7 @@ export class V2E2EECoordinator {
1232
1322
  const client = this.client;
1233
1323
  await client._ensureV2SessionReady('message.pull');
1234
1324
  const ns = client._aid ? `p2p:${client._aid}` : '';
1325
+ let volatileCursor = String(opts?.afterVolatileCursor ?? client._delivery.volatilePullCursor() ?? '').trim();
1235
1326
  if (ns)
1236
1327
  client._delivery.onPullStarted?.(ns);
1237
1328
  if (opts?.windowMode === 'tail') {
@@ -1249,7 +1340,7 @@ export class V2E2EECoordinator {
1249
1340
  }
1250
1341
  if (ns && !opts?.gateLocked) {
1251
1342
  const key = pullGateKeyForClient(client, 'message.v2.pull', {
1252
- after_seq: afterSeq, force: opts?.force === true, limit,
1343
+ after_seq: afterSeq, after_volatile_cursor: volatileCursor, force: opts?.force === true, limit,
1253
1344
  }, `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
1254
1345
  return await client._runPullSerialized(key, () => this.pullV2Impl(afterSeq, limit, {
1255
1346
  ...(opts ?? {}),
@@ -1260,7 +1351,7 @@ export class V2E2EECoordinator {
1260
1351
  let nextAfterSeq = opts?.force ? afterSeq : (afterSeq || (ns ? client._seqTracker.getContiguousSeq(ns) : 0));
1261
1352
  let pullGateKey = ns
1262
1353
  ? pullGateKeyForClient(client, 'message.v2.pull', {
1263
- after_seq: afterSeq, force: opts?.force === true, limit,
1354
+ after_seq: afterSeq, after_volatile_cursor: volatileCursor, force: opts?.force === true, limit,
1264
1355
  }, `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`)
1265
1356
  : '';
1266
1357
  const deferredServerCursor = ns ? this.pendingForwardCursor(ns, client._seqTracker.getContiguousSeq(ns)) : 0;
@@ -1269,239 +1360,389 @@ export class V2E2EECoordinator {
1269
1360
  let lastAutoAckSeq = 0;
1270
1361
  let pendingAckSeq = deferredForwardAck;
1271
1362
  const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
1272
- while (pageCount < maxPages) {
1273
- pageCount += 1;
1274
- const ackUpToSeq = pendingAckSeq;
1275
- const pendingInlineGeneration = ackUpToSeq > 0
1276
- ? client._delivery?.pendingP2PInlineAckGeneration?.(ns) ?? null
1277
- : null;
1278
- const result = await client._callRawV2Rpc('message.v2.pull', {
1279
- after_seq: nextAfterSeq,
1280
- limit,
1281
- ...(opts?.force ? { force: true } : {}),
1282
- ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
1283
- }, pullGateKey);
1284
- if (ackUpToSeq > 0) {
1285
- const actualAckSeq = client._delivery?.resolveP2PPullAckSeq?.(result, ackUpToSeq) ?? 0;
1286
- if (actualAckSeq >= ackUpToSeq) {
1287
- if (pendingInlineGeneration !== null) {
1288
- client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
1363
+ let pipelineRunning = false;
1364
+ let pipelineError = null;
1365
+ const pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
1366
+ let pipelineProducer = null;
1367
+ let pipelineStopped = false;
1368
+ const startPipeline = (startAfterSeq, startVolatileCursor) => {
1369
+ if (pipelineRunning || pageCount >= maxPages || limit < V2_PULL_MAX_PAGE_LIMIT)
1370
+ return;
1371
+ pipelineRunning = true;
1372
+ pipelineProducer = (async () => {
1373
+ let producerAfterSeq = startAfterSeq;
1374
+ let producerVolatileCursor = startVolatileCursor;
1375
+ for (let produced = pageCount; produced < maxPages; produced += 1) {
1376
+ if (pipelineStopped)
1377
+ break;
1378
+ const requestedVolatileCursor = producerVolatileCursor;
1379
+ const result = await client._callRawV2Rpc('message.v2.pull', {
1380
+ after_seq: producerAfterSeq,
1381
+ limit,
1382
+ ...(producerVolatileCursor ? { after_volatile_cursor: producerVolatileCursor } : {}),
1383
+ ...(opts?.force ? { force: true } : {}),
1384
+ }, pullGateKey);
1385
+ if (pipelineStopped)
1386
+ break;
1387
+ const rawPageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1388
+ const enqueued = await pipelineQueue.put({
1389
+ result,
1390
+ afterSeq: producerAfterSeq,
1391
+ ackUpToSeq: 0,
1392
+ inlineAckGeneration: null,
1393
+ requestedVolatileCursor,
1394
+ weight: rawPageMessages.length,
1395
+ }, rawPageMessages.length);
1396
+ if (!enqueued || pipelineStopped)
1397
+ break;
1398
+ const volatileMessages = client._delivery.volatileMessages(rawPageMessages);
1399
+ if (volatileMessages.length > 0)
1400
+ break;
1401
+ const messages = deduplicateForwardPageMessages(rawPageMessages, producerAfterSeq);
1402
+ const seqs = messages
1403
+ .map((msg) => Number(msg.seq ?? 0))
1404
+ .filter((seq) => Number.isFinite(seq) && seq > 0);
1405
+ const nextAfter = Math.max(contiguousPullSeq(producerAfterSeq, seqs, result.contiguous_seq), producerAfterSeq);
1406
+ const cursor = isJsonObject(result.cursor)
1407
+ ? result.cursor : null;
1408
+ const shouldContinue = shouldContinueForwardPage({
1409
+ rawCount: messages.length,
1410
+ nextAfterSeq: producerAfterSeq,
1411
+ nextAfter,
1412
+ limit,
1413
+ hasMoreField: Object.prototype.hasOwnProperty.call(result, 'has_more'),
1414
+ hasMore: result.has_more === true,
1415
+ serverHead: safeNonNegativeSeq(cursor?.latest_seq) ?? 0,
1416
+ observedUpper: Math.max(nextAfter, Number(client._pendingP2pPullUpper?.get?.(ns) ?? 0)),
1417
+ pendingAckSeq: 0,
1418
+ });
1419
+ if (!shouldContinue)
1420
+ break;
1421
+ producerAfterSeq = nextAfter;
1422
+ producerVolatileCursor = '';
1423
+ }
1424
+ })().catch((error) => {
1425
+ pipelineError = error;
1426
+ }).finally(() => {
1427
+ pipelineQueue.close();
1428
+ });
1429
+ };
1430
+ const stopPipeline = async () => {
1431
+ pipelineStopped = true;
1432
+ pipelineQueue.close();
1433
+ if (pipelineProducer) {
1434
+ const producer = pipelineProducer;
1435
+ pipelineProducer = null;
1436
+ await producer;
1437
+ }
1438
+ pipelineRunning = false;
1439
+ };
1440
+ try {
1441
+ while (pageCount < maxPages) {
1442
+ let pipelinePage;
1443
+ if (pipelineRunning) {
1444
+ pipelinePage = await pipelineQueue.take();
1445
+ if (!pipelinePage) {
1446
+ pipelineRunning = false;
1447
+ if (pipelineError)
1448
+ throw pipelineError;
1449
+ break;
1289
1450
  }
1290
- pendingAckSeq = 0;
1291
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1292
- this.clearForwardAck(ns, ackUpToSeq);
1451
+ }
1452
+ pageCount += 1;
1453
+ const volatileCursorBeforePage = pipelinePage?.requestedVolatileCursor ?? volatileCursor;
1454
+ const ackUpToSeq = pipelinePage?.ackUpToSeq ?? pendingAckSeq;
1455
+ const pendingInlineGeneration = pipelinePage?.inlineAckGeneration ?? (ackUpToSeq > 0
1456
+ ? client._delivery?.pendingP2PInlineAckGeneration?.(ns) ?? null
1457
+ : null);
1458
+ let result;
1459
+ if (pipelinePage) {
1460
+ nextAfterSeq = pipelinePage.afterSeq;
1461
+ result = pipelinePage.result;
1293
1462
  }
1294
1463
  else {
1295
- client._clientLog.warn(`message.v2.pull piggyback ACK below target: requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
1296
- await this.confirmForwardP2PAck(ackUpToSeq, pendingInlineGeneration ?? undefined);
1297
- pendingAckSeq = 0;
1298
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1464
+ try {
1465
+ result = await client._callRawV2Rpc('message.v2.pull', {
1466
+ after_seq: nextAfterSeq,
1467
+ limit,
1468
+ ...(volatileCursor ? { after_volatile_cursor: volatileCursor } : {}),
1469
+ ...(opts?.force ? { force: true } : {}),
1470
+ ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
1471
+ }, pullGateKey);
1472
+ }
1473
+ catch (exc) {
1474
+ const text = exc instanceof Error ? exc.message : String(exc);
1475
+ if (!volatileCursor || !text.toLowerCase().includes('volatile cursor epoch mismatch'))
1476
+ throw exc;
1477
+ client._delivery.resetVolatilePullCursor();
1478
+ volatileCursor = '';
1479
+ pageCount -= 1;
1480
+ continue;
1481
+ }
1299
1482
  }
1300
- }
1301
- const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1302
- const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
1303
- const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1304
- const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
1305
- const hasMore = result.has_more === true;
1306
- const serverHead = safeNonNegativeSeq(cursor?.latest_seq) ?? 0;
1307
- client._clientLog.debug(`message.v2.pull page response: page=${pageCount}, raw_count=${messages.length}, stale_count=${Math.max(0, pageMessages.length - messages.length)}, has_more=${String(result.has_more ?? '')}, server_head=${serverHead}, server_ack_seq=${String(result.server_ack_seq ?? '')}`);
1308
- const seqs = messages
1309
- .map((msg) => Number(msg.seq ?? 0))
1310
- .filter((seq) => Number.isFinite(seq) && seq > 0);
1311
- const pageContigBefore = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
1312
- const pageTrackerSnapshot = ns && typeof client._seqTracker.snapshotNamespace === 'function'
1313
- ? client._seqTracker.snapshotNamespace(ns)
1314
- : null;
1315
- const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
1316
- const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
1317
- const deferredKeyFetches = new Map();
1318
- let blockedSeq = 0;
1319
- const pageDecryptedBefore = decrypted.length;
1320
- for (const msg of messages) {
1321
- const seq = Number(msg.seq ?? 0);
1322
- if (!Number.isFinite(seq) || seq <= 0)
1323
- continue;
1324
- const version = String(msg.version ?? 'v2');
1325
- if (version === 'v1') {
1326
- const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
1327
- const legacyPayload = legacy.payload !== undefined ? legacy.payload : msg.payload;
1328
- const payloadType = isJsonObject(legacyPayload)
1329
- ? String(legacyPayload.type ?? '').trim()
1330
- : '';
1331
- if (legacyPayload !== undefined && legacyPayload !== null
1332
- && !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
1333
- const v1Msg = {
1334
- message_id: String(msg.message_id ?? ''),
1335
- from: String(msg.from_aid ?? ''),
1336
- to: String(legacy.to ?? client._aid ?? ''),
1337
- seq: msg.seq,
1338
- type: String(msg.type ?? ''),
1339
- timestamp: msg.t_server,
1340
- payload: legacyPayload,
1341
- encrypted: false,
1342
- };
1343
- attachGatewayProximity(v1Msg, msg);
1344
- const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
1345
- if (ns)
1346
- await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
1347
- else
1348
- await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull');
1349
- decrypted.push(v1Msg);
1483
+ if (ackUpToSeq > 0) {
1484
+ const actualAckSeq = client._delivery?.resolveP2PPullAckSeq?.(result, ackUpToSeq) ?? 0;
1485
+ if (actualAckSeq >= ackUpToSeq) {
1486
+ if (pendingInlineGeneration !== null) {
1487
+ client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
1488
+ }
1489
+ pendingAckSeq = 0;
1490
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1491
+ this.clearForwardAck(ns, ackUpToSeq);
1350
1492
  }
1351
- else if (isJsonObject(legacyPayload)
1352
- && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1353
- const deferStatus = {};
1354
- const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(legacyPayload) }, true, true, true, true, deferStatus, true, 'pull');
1355
- if (deferStatus.deferred) {
1356
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1357
- if (deferStatus.fromAid) {
1358
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1359
- deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
1493
+ else {
1494
+ client._clientLog.warn(`message.v2.pull piggyback ACK below target: requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
1495
+ await this.confirmForwardP2PAck(ackUpToSeq, pendingInlineGeneration ?? undefined);
1496
+ pendingAckSeq = 0;
1497
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1498
+ }
1499
+ }
1500
+ const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1501
+ const volatileMessages = client._delivery.volatileMessages(pageMessages);
1502
+ if (volatileMessages.length > 0) {
1503
+ decrypted.push(...await this.publishV2VolatileMessages(volatileMessages, 'pull', true));
1504
+ volatileCursor = client._delivery.volatilePullCursor();
1505
+ }
1506
+ const volatileAdvanced = Boolean(volatileCursor) && volatileCursor !== volatileCursorBeforePage;
1507
+ const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
1508
+ const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1509
+ const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
1510
+ const hasMore = result.has_more === true;
1511
+ const serverHead = safeNonNegativeSeq(cursor?.latest_seq) ?? 0;
1512
+ client._clientLog.debug(`message.v2.pull page response: page=${pageCount}, raw_count=${messages.length}, stale_count=${Math.max(0, pageMessages.length - messages.length)}, has_more=${String(result.has_more ?? '')}, server_head=${serverHead}, server_ack_seq=${String(result.server_ack_seq ?? '')}`);
1513
+ const seqs = messages
1514
+ .map((msg) => Number(msg.seq ?? 0))
1515
+ .filter((seq) => Number.isFinite(seq) && seq > 0);
1516
+ const pageContigBefore = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
1517
+ const pageTrackerSnapshot = ns && typeof client._seqTracker.snapshotNamespace === 'function'
1518
+ ? client._seqTracker.snapshotNamespace(ns)
1519
+ : null;
1520
+ const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
1521
+ const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
1522
+ const deferredKeyFetches = new Map();
1523
+ let blockedSeq = 0;
1524
+ const pageDecryptedBefore = decrypted.length;
1525
+ for (const msg of messages) {
1526
+ const seq = Number(msg.seq ?? 0);
1527
+ if (!Number.isFinite(seq) || seq <= 0)
1528
+ continue;
1529
+ const version = String(msg.version ?? 'v2');
1530
+ if (version === 'v1') {
1531
+ const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
1532
+ const legacyPayload = legacy.payload !== undefined ? legacy.payload : msg.payload;
1533
+ const payloadType = isJsonObject(legacyPayload)
1534
+ ? String(legacyPayload.type ?? '').trim()
1535
+ : '';
1536
+ if (legacyPayload !== undefined && legacyPayload !== null
1537
+ && !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
1538
+ const v1Msg = {
1539
+ message_id: String(msg.message_id ?? ''),
1540
+ from: String(msg.from_aid ?? ''),
1541
+ to: String(legacy.to ?? client._aid ?? ''),
1542
+ seq: msg.seq,
1543
+ type: String(msg.type ?? ''),
1544
+ timestamp: msg.t_server,
1545
+ payload: legacyPayload,
1546
+ encrypted: false,
1547
+ };
1548
+ const direction = String(msg.direction ?? '').trim();
1549
+ if (direction)
1550
+ v1Msg.direction = direction;
1551
+ else if (String(msg.from_aid ?? '').trim() && String(msg.from_aid ?? '').trim() !== String(client._aid ?? '').trim()) {
1552
+ v1Msg.direction = 'inbound';
1360
1553
  }
1361
- }
1362
- if (plaintext) {
1554
+ if (msg.sender_device_id !== undefined)
1555
+ v1Msg.sender_device_id = msg.sender_device_id;
1556
+ attachGatewayProximity(v1Msg, msg);
1557
+ const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
1363
1558
  if (ns)
1364
- await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1559
+ await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
1365
1560
  else
1366
- await client._publishAppEvent('message.received', plaintext, 'pull');
1367
- decrypted.push(plaintext);
1561
+ await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull');
1562
+ decrypted.push(v1Msg);
1563
+ }
1564
+ else if (isJsonObject(legacyPayload)
1565
+ && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1566
+ const deferStatus = {};
1567
+ const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(legacyPayload) }, true, true, true, true, deferStatus, true, 'pull');
1568
+ if (deferStatus.deferred) {
1569
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1570
+ if (deferStatus.fromAid) {
1571
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1572
+ deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
1573
+ }
1574
+ }
1575
+ if (plaintext) {
1576
+ if (ns)
1577
+ await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1578
+ else
1579
+ await client._publishAppEvent('message.received', plaintext, 'pull');
1580
+ decrypted.push(plaintext);
1581
+ }
1582
+ }
1583
+ else {
1584
+ client._clientLog.debug(`message.v2.pull skipping V1 envelope seq=${seq} payload_type=${payloadType || '<none>'} (V1 E2EE removed)`);
1368
1585
  }
1586
+ continue;
1587
+ }
1588
+ if (version !== 'v2') {
1589
+ client._clientLog.debug(`message.v2.pull skipping non-V2 row seq=${seq} version=${String(msg.version ?? '')}`);
1590
+ continue;
1591
+ }
1592
+ const msgSpkId = String(msg.spk_id ?? '');
1593
+ if (msgSpkId && client._v2Session && !client._v2Session.isCurrentSPK(msgSpkId)) {
1594
+ client._v2Session.trackOldSPKMaxSeq(msgSpkId, seq);
1595
+ }
1596
+ const deferStatus = {};
1597
+ const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
1598
+ if (deferStatus.deferred) {
1599
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1600
+ }
1601
+ if (deferStatus.deferred && deferStatus.fromAid) {
1602
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1603
+ deferredKeyFetches.set(key, {
1604
+ fromAid: deferStatus.fromAid,
1605
+ senderDeviceId: deferStatus.senderDeviceId ?? '',
1606
+ groupId: deferStatus.groupId ?? '',
1607
+ });
1608
+ }
1609
+ if (plaintext === null)
1610
+ continue;
1611
+ if (ns) {
1612
+ await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1613
+ decrypted.push(plaintext);
1369
1614
  }
1370
1615
  else {
1371
- client._clientLog.debug(`message.v2.pull skipping V1 envelope seq=${seq} payload_type=${payloadType || '<none>'} (V1 E2EE removed)`);
1616
+ await client._publishAppEvent('message.received', plaintext, 'pull');
1617
+ decrypted.push(plaintext);
1372
1618
  }
1373
- continue;
1374
- }
1375
- if (version !== 'v2') {
1376
- client._clientLog.debug(`message.v2.pull skipping non-V2 row seq=${seq} version=${String(msg.version ?? '')}`);
1377
- continue;
1378
- }
1379
- const msgSpkId = String(msg.spk_id ?? '');
1380
- if (msgSpkId && client._v2Session && !client._v2Session.isCurrentSPK(msgSpkId)) {
1381
- client._v2Session.trackOldSPKMaxSeq(msgSpkId, seq);
1382
1619
  }
1383
- const deferStatus = {};
1384
- const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
1385
- if (deferStatus.deferred) {
1386
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1387
- }
1388
- if (deferStatus.deferred && deferStatus.fromAid) {
1389
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1390
- deferredKeyFetches.set(key, {
1391
- fromAid: deferStatus.fromAid,
1392
- senderDeviceId: deferStatus.senderDeviceId ?? '',
1393
- groupId: deferStatus.groupId ?? '',
1394
- });
1395
- }
1396
- if (plaintext === null)
1397
- continue;
1620
+ const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
1621
+ const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
1622
+ const serverAckSeq = parsedServerAckSeq ?? 0;
1623
+ const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
1398
1624
  if (ns) {
1399
- await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1400
- decrypted.push(plaintext);
1401
- }
1402
- else {
1403
- await client._publishAppEvent('message.received', plaintext, 'pull');
1404
- decrypted.push(plaintext);
1405
- }
1406
- }
1407
- const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
1408
- const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
1409
- const serverAckSeq = parsedServerAckSeq ?? 0;
1410
- const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
1411
- if (ns) {
1412
- const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, serverAckSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
1413
- let ackSeq = pageContigBefore;
1414
- let contigAdvanced = false;
1415
- try {
1416
- if (commitTarget > pageContigBefore) {
1417
- client._seqTracker.forceContiguousSeq(ns, commitTarget);
1418
- if (retentionFloor > pageContigBefore) {
1419
- client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${pageContigBefore} -> retention_floor_seq=${retentionFloor}`);
1625
+ const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, serverAckSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
1626
+ let ackSeq = pageContigBefore;
1627
+ let contigAdvanced = false;
1628
+ try {
1629
+ if (commitTarget > pageContigBefore) {
1630
+ client._seqTracker.forceContiguousSeq(ns, commitTarget);
1631
+ if (retentionFloor > pageContigBefore) {
1632
+ client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${pageContigBefore} -> retention_floor_seq=${retentionFloor}`);
1633
+ }
1634
+ if (deferredServerCursor > pageContigBefore && pageCount === 1) {
1635
+ client._clientLog.info(`message.v2.pull restore local A from deferred server ACK cursor: ns=${ns}, contiguous=${pageContigBefore} -> server_ack_seq=${deferredServerCursor}`);
1636
+ }
1637
+ if (seqs.length > 0) {
1638
+ client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
1639
+ }
1420
1640
  }
1421
- if (deferredServerCursor > pageContigBefore && pageCount === 1) {
1422
- client._clientLog.info(`message.v2.pull restore local A from deferred server ACK cursor: ns=${ns}, contiguous=${pageContigBefore} -> server_ack_seq=${deferredServerCursor}`);
1641
+ ackSeq = client._seqTracker.getContiguousSeq(ns);
1642
+ contigAdvanced = ackSeq !== pageContigBefore;
1643
+ if (contigAdvanced) {
1644
+ await client._commitSeqTrackerState(ns);
1645
+ if (pageCount === 1 && deferredServerCursor > 0)
1646
+ this.clearForwardCursor(ns, ackSeq);
1423
1647
  }
1424
- if (seqs.length > 0) {
1425
- client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
1648
+ }
1649
+ catch (exc) {
1650
+ if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
1651
+ client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
1426
1652
  }
1653
+ client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
1654
+ throw exc;
1427
1655
  }
1428
- ackSeq = client._seqTracker.getContiguousSeq(ns);
1429
- contigAdvanced = ackSeq !== pageContigBefore;
1430
- if (contigAdvanced) {
1431
- await client._commitSeqTrackerState(ns);
1432
- if (pageCount === 1 && deferredServerCursor > 0)
1433
- this.clearForwardCursor(ns, ackSeq);
1656
+ if (contigAdvanced)
1657
+ await client._drainOrderedMessages(ns, undefined, false, false);
1658
+ for (const fetch of deferredKeyFetches.values()) {
1659
+ this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1434
1660
  }
1435
- }
1436
- catch (exc) {
1437
- if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
1438
- client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
1661
+ if (parsedServerAckSeq !== null && serverAckSeq > ackSeq) {
1662
+ this.recordForwardCursor(ns, serverAckSeq, ackSeq);
1663
+ }
1664
+ // 无本页服务端水位时,只有本页成功提交了比已知服务端更高的 A
1665
+ // 才新增 ACK 义务。空页不能仅因本地已有 A 而重复 ACK。
1666
+ const knownServerAckSeq = hasServerAckSeq
1667
+ ? serverAckSeq
1668
+ : (pageCount === 1 ? deferredServerCursor : 0);
1669
+ const ackNeeded = ackSeq > 0
1670
+ && ackSeq > lastAutoAckSeq
1671
+ && ((hasServerAckSeq && ackSeq > serverAckSeq)
1672
+ || (contigAdvanced && ackSeq > knownServerAckSeq));
1673
+ if (ackNeeded) {
1674
+ this.recordForwardAck(ns, ackSeq);
1675
+ const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
1676
+ const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq;
1677
+ if (canContinuePage && !pipelineRunning) {
1678
+ pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
1679
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1680
+ }
1681
+ else {
1682
+ await this.confirmForwardP2PAck(ackSeq);
1683
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1684
+ }
1439
1685
  }
1440
- client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
1441
- throw exc;
1442
- }
1443
- if (contigAdvanced)
1444
- await client._drainOrderedMessages(ns, undefined, false, false);
1445
- for (const fetch of deferredKeyFetches.values()) {
1446
- this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1447
1686
  }
1448
- if (parsedServerAckSeq !== null && serverAckSeq > ackSeq) {
1449
- this.recordForwardCursor(ns, serverAckSeq, ackSeq);
1687
+ if (ns)
1688
+ client._delivery.onPullPage?.(ns, decrypted.length - pageDecryptedBefore, hasMore, pageMaxSeq, typeof result.remaining === 'number' && Number.isSafeInteger(result.remaining) && result.remaining >= 0 ? result.remaining : null, messages.length, client._seqTracker.getContiguousSeq(ns), pageMaxSeq);
1689
+ const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
1690
+ const rawCount = messages.length;
1691
+ if (blockedSeq > 0)
1692
+ break;
1693
+ let shouldContinue = shouldContinueForwardPage({
1694
+ rawCount,
1695
+ nextAfterSeq,
1696
+ nextAfter,
1697
+ limit,
1698
+ hasMoreField,
1699
+ hasMore,
1700
+ serverHead,
1701
+ pendingAckSeq,
1702
+ observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
1703
+ ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
1704
+ });
1705
+ const volatileHasMoreField = Object.prototype.hasOwnProperty.call(result, 'volatile_has_more');
1706
+ const volatileHasMore = result.volatile_has_more === true;
1707
+ if (!shouldContinue && volatileAdvanced && (volatileHasMore || hasMore || (!volatileHasMoreField && limit > 0 && pageMessages.length >= limit))) {
1708
+ shouldContinue = true;
1450
1709
  }
1451
- // 无本页服务端水位时,只有本页成功提交了比已知服务端更高的 A
1452
- // 才新增 ACK 义务。空页不能仅因本地已有 A 而重复 ACK。
1453
- const knownServerAckSeq = hasServerAckSeq
1454
- ? serverAckSeq
1455
- : (pageCount === 1 ? deferredServerCursor : 0);
1456
- const ackNeeded = ackSeq > 0
1457
- && ackSeq > lastAutoAckSeq
1458
- && ((hasServerAckSeq && ackSeq > serverAckSeq)
1459
- || (contigAdvanced && ackSeq > knownServerAckSeq));
1460
- if (ackNeeded) {
1461
- this.recordForwardAck(ns, ackSeq);
1462
- const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
1463
- const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq;
1464
- if (canContinuePage) {
1465
- pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
1466
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1467
- }
1468
- else {
1469
- await this.confirmForwardP2PAck(ackSeq);
1470
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1710
+ const shouldContinueForPipeline = shouldContinueForwardPage({
1711
+ rawCount,
1712
+ nextAfterSeq,
1713
+ nextAfter,
1714
+ limit,
1715
+ hasMoreField,
1716
+ hasMore,
1717
+ serverHead,
1718
+ pendingAckSeq: 0,
1719
+ observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
1720
+ ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
1721
+ });
1722
+ if (!shouldContinue)
1723
+ break;
1724
+ if (shouldContinueForPipeline && !volatileAdvanced && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
1725
+ if (pendingAckSeq > 0) {
1726
+ await this.confirmForwardP2PAck(pendingAckSeq);
1727
+ pendingAckSeq = 0;
1471
1728
  }
1729
+ startPipeline(nextAfter, volatileCursor);
1730
+ }
1731
+ if (!pipelineRunning && pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
1732
+ const nextKey = pullGateKeyForClient(client, 'message.v2.pull', {
1733
+ after_seq: nextAfter, after_volatile_cursor: volatileCursor, force: opts?.force === true, limit,
1734
+ }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
1735
+ await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
1736
+ pullGateKey = nextKey;
1472
1737
  }
1738
+ const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
1739
+ if (tailDelayMs > 0)
1740
+ await v2Sleep(tailDelayMs);
1741
+ nextAfterSeq = nextAfter;
1473
1742
  }
1474
- if (ns)
1475
- client._delivery.onPullPage?.(ns, decrypted.length - pageDecryptedBefore, hasMore, pageMaxSeq, typeof result.remaining === 'number' && Number.isSafeInteger(result.remaining) && result.remaining >= 0 ? result.remaining : null, messages.length, client._seqTracker.getContiguousSeq(ns), pageMaxSeq);
1476
- const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
1477
- const rawCount = messages.length;
1478
- if (blockedSeq > 0)
1479
- break;
1480
- const shouldContinue = shouldContinueForwardPage({
1481
- rawCount,
1482
- nextAfterSeq,
1483
- nextAfter,
1484
- limit,
1485
- hasMoreField,
1486
- hasMore,
1487
- serverHead,
1488
- pendingAckSeq,
1489
- observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
1490
- ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
1491
- });
1492
- if (!shouldContinue)
1493
- break;
1494
- if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
1495
- const nextKey = pullGateKeyForClient(client, 'message.v2.pull', {
1496
- after_seq: nextAfter, force: opts?.force === true, limit,
1497
- }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
1498
- await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
1499
- pullGateKey = nextKey;
1500
- }
1501
- const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
1502
- if (tailDelayMs > 0)
1503
- await v2Sleep(tailDelayMs);
1504
- nextAfterSeq = nextAfter;
1743
+ }
1744
+ finally {
1745
+ await stopPipeline();
1505
1746
  }
1506
1747
  if (pageCount >= maxPages) {
1507
1748
  client._clientLog.warn(`message.v2.pull reached max_pages=${maxPages} after_seq=${nextAfterSeq}`);
@@ -1513,6 +1754,73 @@ export class V2E2EECoordinator {
1513
1754
  client._delivery.onPullWorkSettled?.();
1514
1755
  return deduplicatePulledResults(decrypted);
1515
1756
  }
1757
+ async publishV2VolatileMessages(messages, source, advanceCursor) {
1758
+ const client = this.client;
1759
+ if (!client._v2Session)
1760
+ return [];
1761
+ const processed = [];
1762
+ const plaintextMessages = [];
1763
+ let complete = true;
1764
+ for (const rawMessage of messages) {
1765
+ const claim = client._delivery.beginVolatileDelivery(rawMessage);
1766
+ if (claim === 'seen') {
1767
+ processed.push(rawMessage);
1768
+ continue;
1769
+ }
1770
+ if (claim === 'inflight') {
1771
+ complete = false;
1772
+ continue;
1773
+ }
1774
+ let plaintext;
1775
+ try {
1776
+ if (client._isEncryptedPushMessage(rawMessage)) {
1777
+ plaintext = await client._decryptV2Message(rawMessage, true, true, true, true, undefined, false, source);
1778
+ }
1779
+ else {
1780
+ const legacy = isJsonObject(rawMessage.legacy_v1)
1781
+ ? rawMessage.legacy_v1
1782
+ : {};
1783
+ plaintext = {
1784
+ message_id: String(rawMessage.message_id ?? ''),
1785
+ from: String(rawMessage.from_aid ?? rawMessage.from ?? ''),
1786
+ to: String(legacy.to ?? rawMessage.to ?? client._aid ?? ''),
1787
+ type: String(rawMessage.type ?? ''),
1788
+ timestamp: rawMessage.t_server ?? rawMessage.timestamp,
1789
+ payload: legacy.payload !== undefined ? legacy.payload : rawMessage.payload,
1790
+ encrypted: false,
1791
+ };
1792
+ for (const key of ['context', 'protected_headers', 'headers', 'client_signature']) {
1793
+ if (key in rawMessage)
1794
+ plaintext[key] = rawMessage[key];
1795
+ }
1796
+ if (rawMessage.sender_device_id !== undefined)
1797
+ plaintext.sender_device_id = rawMessage.sender_device_id;
1798
+ attachGatewayProximity(plaintext, rawMessage);
1799
+ }
1800
+ if (!plaintext) {
1801
+ complete = false;
1802
+ continue;
1803
+ }
1804
+ const publicMessage = client._delivery.publicVolatileMessage(plaintext);
1805
+ for (const key of ['storage', 'volatile_cursor', 'created_at', 'expires_at', 'persist']) {
1806
+ if (key in rawMessage)
1807
+ publicMessage[key] = rawMessage[key];
1808
+ }
1809
+ const appEvent = client._delivery.p2pAppEventForMessage(publicMessage);
1810
+ await client._publishAppEvent(appEvent.event, appEvent.payload, source);
1811
+ client._delivery.markVolatileSeen(rawMessage);
1812
+ processed.push(rawMessage);
1813
+ plaintextMessages.push(publicMessage);
1814
+ }
1815
+ finally {
1816
+ client._delivery.finishVolatileDelivery(rawMessage);
1817
+ }
1818
+ }
1819
+ if (processed.length > 0) {
1820
+ await client._delivery.ackVolatileMessages(processed, true, advanceCursor && complete && processed.length === messages.length);
1821
+ }
1822
+ return plaintextMessages;
1823
+ }
1516
1824
  async confirmForwardP2PAck(upToSeq, inlineGeneration) {
1517
1825
  const ns = this.client._aid ? `p2p:${this.client._aid}` : '';
1518
1826
  try {
@@ -1528,41 +1836,51 @@ export class V2E2EECoordinator {
1528
1836
  throw exc;
1529
1837
  }
1530
1838
  }
1531
- async ackV2(upToSeq, inlineGeneration) {
1532
- return this.ackV2WithConfirmation(upToSeq, inlineGeneration, false);
1839
+ async ackV2(upToSeq, inlineGeneration, volatileCursors = [], volatileIds = []) {
1840
+ return this.ackV2WithConfirmation(upToSeq, inlineGeneration, false, volatileCursors, volatileIds);
1533
1841
  }
1534
1842
  async ackV2ForForward(upToSeq, inlineGeneration) {
1535
1843
  return this.ackV2WithConfirmation(upToSeq, inlineGeneration, true);
1536
1844
  }
1537
- async ackV2WithConfirmation(upToSeq, inlineGeneration, requireServerWatermark) {
1845
+ async ackV2WithConfirmation(upToSeq, inlineGeneration, requireServerWatermark, volatileCursors = [], volatileIds = []) {
1538
1846
  const client = this.client;
1539
1847
  const ns = client._aid ? `p2p:${client._aid}` : '';
1540
1848
  let seq = upToSeq ?? (ns ? client._seqTracker.getContiguousSeq(ns) : 0);
1541
1849
  if (typeof seq !== 'number' || !Number.isSafeInteger(seq) || seq <= 0)
1850
+ seq = 0;
1851
+ if (seq <= 0 && volatileCursors.length === 0 && volatileIds.length === 0)
1542
1852
  return { acked: 0 };
1543
- seq = client._clampAckSeq('message.v2.ack', 'up_to_seq', ns, seq);
1544
- if (!Number.isSafeInteger(seq) || seq <= 0)
1853
+ if (seq > 0)
1854
+ seq = client._clampAckSeq('message.v2.ack', 'up_to_seq', ns, seq);
1855
+ if ((!Number.isSafeInteger(seq) || seq <= 0) && volatileCursors.length === 0 && volatileIds.length === 0) {
1545
1856
  return { acked: 0 };
1857
+ }
1546
1858
  // ACK 发出前绑定当时的 inline pending;迟到响应不得结算重置后新建的 pending。
1547
1859
  const ackGeneration = inlineGeneration ?? client._delivery?.pendingP2PInlineAckGeneration?.(ns) ?? null;
1548
1860
  const v2SessionAtStart = client._v2Session;
1549
1861
  const v2RuntimeGenerationAtStart = Number(client._v2RuntimeGeneration ?? 0);
1550
- const raw = await client._callRawV2Rpc('message.v2.ack', { up_to_seq: seq });
1862
+ const raw = await client._callRawV2Rpc('message.v2.ack', {
1863
+ ...(seq > 0 ? { up_to_seq: seq } : {}),
1864
+ ...(volatileCursors.length > 0 ? { volatile_cursors: volatileCursors } : {}),
1865
+ ...(volatileIds.length > 0 ? { volatile_ids: volatileIds } : {}),
1866
+ });
1551
1867
  const result = isJsonObject(raw)
1552
1868
  ? { ...raw }
1553
1869
  : { result: raw };
1554
- const actualAckSeq = requireServerWatermark
1555
- ? (client._delivery?.resolveForwardAckSeq?.(result, seq) ?? 0)
1556
- : (client._delivery?.resolveActualAckSeq?.(result, seq) ?? seq);
1557
- if (requireServerWatermark && actualAckSeq < seq) {
1870
+ const actualAckSeq = seq > 0
1871
+ ? (requireServerWatermark
1872
+ ? (client._delivery?.resolveForwardAckSeq?.(result, seq) ?? 0)
1873
+ : (client._delivery?.resolveActualAckSeq?.(result, seq) ?? seq))
1874
+ : 0;
1875
+ if (seq > 0 && requireServerWatermark && actualAckSeq < seq) {
1558
1876
  throw new E2EEError(`message.v2.ack server ACK watermark ${actualAckSeq} is below requested ${seq}`);
1559
1877
  }
1560
1878
  result.ack_seq = actualAckSeq;
1561
1879
  result.success = true;
1562
- if (ackGeneration !== null) {
1880
+ if (seq > 0 && ackGeneration !== null) {
1563
1881
  client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, ackGeneration);
1564
1882
  }
1565
- if (v2SessionAtStart
1883
+ if (seq > 0 && v2SessionAtStart
1566
1884
  && client._v2Session === v2SessionAtStart
1567
1885
  && Number(client._v2RuntimeGeneration ?? 0) === v2RuntimeGenerationAtStart) {
1568
1886
  try {
@@ -1794,83 +2112,197 @@ export class V2E2EECoordinator {
1794
2112
  let lastAutoAckSeq = 0;
1795
2113
  let pendingAckSeq = deferredForwardAck;
1796
2114
  const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
1797
- while (pageCount < maxPages) {
1798
- pageCount += 1;
1799
- const ackUpToSeq = ownsCursor ? pendingAckSeq : 0;
1800
- const pendingInlineGeneration = ackUpToSeq > 0
1801
- ? client._delivery?.pendingGroupInlineAckGeneration?.(ns) ?? null
1802
- : null;
1803
- const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
1804
- group_id: wireGroupId,
1805
- after_seq: nextAfterSeq,
1806
- limit,
1807
- ...cursorParams,
1808
- ...(opts?.force ? { force: true } : {}),
1809
- ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
1810
- }, groupAid), pullGateKey);
1811
- if (ackUpToSeq > 0) {
1812
- const actualAckSeq = client._delivery?.resolveGroupPullAckSeq?.(result, ackUpToSeq) ?? 0;
1813
- if (actualAckSeq >= ackUpToSeq) {
1814
- if (pendingInlineGeneration !== null) {
1815
- client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
2115
+ let pipelineRunning = false;
2116
+ let pipelineError = null;
2117
+ const pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
2118
+ let pipelineProducer = null;
2119
+ let pipelineStopped = false;
2120
+ const startPipeline = (startAfterSeq) => {
2121
+ if (pipelineRunning || pageCount >= maxPages || limit < V2_PULL_MAX_PAGE_LIMIT || !ownsCursor)
2122
+ return;
2123
+ pipelineRunning = true;
2124
+ pipelineProducer = (async () => {
2125
+ let producerAfterSeq = startAfterSeq;
2126
+ for (let produced = pageCount; produced < maxPages; produced += 1) {
2127
+ if (pipelineStopped)
2128
+ break;
2129
+ const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2130
+ group_id: wireGroupId,
2131
+ after_seq: producerAfterSeq,
2132
+ limit,
2133
+ ...cursorParams,
2134
+ ...(opts?.force ? { force: true } : {}),
2135
+ }, groupAid), pullGateKey);
2136
+ if (pipelineStopped)
2137
+ break;
2138
+ const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
2139
+ const enqueued = await pipelineQueue.put({
2140
+ result,
2141
+ afterSeq: producerAfterSeq,
2142
+ ackUpToSeq: 0,
2143
+ inlineAckGeneration: null,
2144
+ requestedVolatileCursor: '',
2145
+ weight: pageMessages.length,
2146
+ }, pageMessages.length);
2147
+ if (!enqueued || pipelineStopped)
2148
+ break;
2149
+ const messages = deduplicateForwardPageMessages(pageMessages, producerAfterSeq);
2150
+ const seqs = messages.map((msg) => Number(msg.seq ?? 0)).filter((seq) => Number.isFinite(seq) && seq > 0);
2151
+ const nextAfter = Math.max(contiguousPullSeq(producerAfterSeq, seqs, result.contiguous_seq), producerAfterSeq);
2152
+ const cursor = isJsonObject(result.cursor)
2153
+ ? result.cursor : null;
2154
+ const shouldContinue = shouldContinueForwardPage({
2155
+ rawCount: messages.length,
2156
+ nextAfterSeq: producerAfterSeq,
2157
+ nextAfter,
2158
+ limit,
2159
+ hasMoreField: Object.prototype.hasOwnProperty.call(result, 'has_more'),
2160
+ hasMore: result.has_more === true,
2161
+ serverHead: Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0),
2162
+ observedUpper: Math.max(nextAfter, typeof client._seqTracker.getMaxSeenSeq === 'function' ? client._seqTracker.getMaxSeenSeq(ns) : 0),
2163
+ pendingAckSeq: 0,
2164
+ });
2165
+ if (!shouldContinue)
2166
+ break;
2167
+ producerAfterSeq = nextAfter;
2168
+ }
2169
+ })().catch((error) => {
2170
+ pipelineError = error;
2171
+ }).finally(() => {
2172
+ pipelineQueue.close();
2173
+ });
2174
+ };
2175
+ const stopPipeline = async () => {
2176
+ pipelineStopped = true;
2177
+ pipelineQueue.close();
2178
+ if (pipelineProducer) {
2179
+ const producer = pipelineProducer;
2180
+ pipelineProducer = null;
2181
+ await producer;
2182
+ }
2183
+ pipelineRunning = false;
2184
+ };
2185
+ try {
2186
+ while (pageCount < maxPages) {
2187
+ let pipelinePage;
2188
+ if (pipelineRunning) {
2189
+ pipelinePage = await pipelineQueue.take();
2190
+ if (!pipelinePage) {
2191
+ pipelineRunning = false;
2192
+ if (pipelineError)
2193
+ throw pipelineError;
2194
+ break;
1816
2195
  }
1817
- pendingAckSeq = 0;
1818
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1819
- this.clearForwardAck(ns, ackUpToSeq);
2196
+ }
2197
+ pageCount += 1;
2198
+ const ackUpToSeq = ownsCursor ? (pipelinePage?.ackUpToSeq ?? pendingAckSeq) : 0;
2199
+ const pendingInlineGeneration = pipelinePage?.inlineAckGeneration ?? (ackUpToSeq > 0
2200
+ ? client._delivery?.pendingGroupInlineAckGeneration?.(ns) ?? null
2201
+ : null);
2202
+ let result;
2203
+ if (pipelinePage) {
2204
+ nextAfterSeq = pipelinePage.afterSeq;
2205
+ result = pipelinePage.result;
1820
2206
  }
1821
2207
  else {
1822
- client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
1823
- await this.confirmForwardGroupAck(gid, ackUpToSeq, groupAid);
1824
- pendingAckSeq = 0;
1825
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
2208
+ result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2209
+ group_id: wireGroupId,
2210
+ after_seq: nextAfterSeq,
2211
+ limit,
2212
+ ...cursorParams,
2213
+ ...(opts?.force ? { force: true } : {}),
2214
+ ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
2215
+ }, groupAid), pullGateKey);
1826
2216
  }
1827
- }
1828
- const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
1829
- if (resultGroupAid)
1830
- groupAid = resultGroupAid;
1831
- const eventGroupAid = resultGroupAid || groupAid || gid;
1832
- const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1833
- const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
1834
- const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1835
- const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
1836
- const hasMore = result.has_more === true;
1837
- const serverHead = Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0);
1838
- const seqs = messages
1839
- .map((msg) => Number(msg.seq ?? 0))
1840
- .filter((seq) => Number.isFinite(seq) && seq > 0);
1841
- const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
1842
- const pageTrackerSnapshot = typeof client._seqTracker.snapshotNamespace === 'function'
1843
- ? client._seqTracker.snapshotNamespace(ns)
1844
- : null;
1845
- const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
1846
- const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
1847
- const deferredKeyFetches = new Map();
1848
- let blockedSeq = 0;
1849
- const pageDecryptedBefore = decrypted.length;
1850
- for (const msg of messages) {
1851
- const seq = Number(msg.seq ?? 0);
1852
- if (!Number.isFinite(seq) || seq <= 0)
1853
- continue;
1854
- const version = String(msg.version ?? 'v2');
1855
- if (version === 'v1') {
1856
- const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
1857
- const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
1858
- const payloadObj = isJsonObject(payload) ? payload : null;
1859
- // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
1860
- if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
1861
- && client._delivery.recallEventFromGroupMessage(msg)) {
1862
- await client._delivery.publishGroupRecallTombstone(gid, seq, {
1863
- ...msg,
1864
- group_id: gid,
1865
- group_aid: eventGroupAid,
1866
- }, 'pull');
1867
- client._markPublishedSeq(ns, seq);
1868
- client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
1869
- continue;
2217
+ if (ackUpToSeq > 0) {
2218
+ const actualAckSeq = client._delivery?.resolveGroupPullAckSeq?.(result, ackUpToSeq) ?? 0;
2219
+ if (actualAckSeq >= ackUpToSeq) {
2220
+ if (pendingInlineGeneration !== null) {
2221
+ client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
2222
+ }
2223
+ pendingAckSeq = 0;
2224
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
2225
+ this.clearForwardAck(ns, ackUpToSeq);
1870
2226
  }
1871
- if (payloadObj) {
1872
- const payloadType = String(payloadObj.type ?? '').trim();
1873
- if (payloadType !== 'e2ee.encrypted' && payloadType !== 'e2ee.group_encrypted' && payloadType !== 'e2ee.p2p_encrypted') {
2227
+ else {
2228
+ client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
2229
+ await this.confirmForwardGroupAck(gid, ackUpToSeq, groupAid);
2230
+ pendingAckSeq = 0;
2231
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
2232
+ }
2233
+ }
2234
+ const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
2235
+ if (resultGroupAid)
2236
+ groupAid = resultGroupAid;
2237
+ const eventGroupAid = resultGroupAid || groupAid || gid;
2238
+ const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
2239
+ const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
2240
+ const cursor = isJsonObject(result.cursor) ? result.cursor : null;
2241
+ const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
2242
+ const hasMore = result.has_more === true;
2243
+ const serverHead = Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0);
2244
+ const seqs = messages
2245
+ .map((msg) => Number(msg.seq ?? 0))
2246
+ .filter((seq) => Number.isFinite(seq) && seq > 0);
2247
+ const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
2248
+ const pageTrackerSnapshot = typeof client._seqTracker.snapshotNamespace === 'function'
2249
+ ? client._seqTracker.snapshotNamespace(ns)
2250
+ : null;
2251
+ const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
2252
+ const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
2253
+ const deferredKeyFetches = new Map();
2254
+ let blockedSeq = 0;
2255
+ const pageDecryptedBefore = decrypted.length;
2256
+ for (const msg of messages) {
2257
+ const seq = Number(msg.seq ?? 0);
2258
+ if (!Number.isFinite(seq) || seq <= 0)
2259
+ continue;
2260
+ const version = String(msg.version ?? 'v2');
2261
+ if (version === 'v1') {
2262
+ const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
2263
+ const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
2264
+ const payloadObj = isJsonObject(payload) ? payload : null;
2265
+ // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
2266
+ if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
2267
+ && client._delivery.recallEventFromGroupMessage(msg)) {
2268
+ await client._delivery.publishGroupRecallTombstone(gid, seq, {
2269
+ ...msg,
2270
+ group_id: gid,
2271
+ group_aid: eventGroupAid,
2272
+ }, 'pull');
2273
+ client._markPublishedSeq(ns, seq);
2274
+ client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
2275
+ continue;
2276
+ }
2277
+ if (payloadObj) {
2278
+ const payloadType = String(payloadObj.type ?? '').trim();
2279
+ if (payloadType !== 'e2ee.encrypted' && payloadType !== 'e2ee.group_encrypted' && payloadType !== 'e2ee.p2p_encrypted') {
2280
+ let v1Msg = {
2281
+ message_id: String(msg.message_id ?? ''),
2282
+ from: String(msg.from_aid ?? ''),
2283
+ group_id: gid,
2284
+ group_aid: eventGroupAid,
2285
+ seq: msg.seq,
2286
+ type: String(msg.type ?? ''),
2287
+ timestamp: msg.t_server,
2288
+ payload,
2289
+ encrypted: false,
2290
+ };
2291
+ if (msg.sender_device_id !== undefined)
2292
+ v1Msg.sender_device_id = msg.sender_device_id;
2293
+ v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
2294
+ attachGatewayProximity(v1Msg, msg);
2295
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2296
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2297
+ }
2298
+ else {
2299
+ client._markPublishedSeq(ns, seq);
2300
+ }
2301
+ decrypted.push(v1Msg);
2302
+ continue;
2303
+ }
2304
+ }
2305
+ else if (payload !== undefined && payload !== null) {
1874
2306
  let v1Msg = {
1875
2307
  message_id: String(msg.message_id ?? ''),
1876
2308
  from: String(msg.from_aid ?? ''),
@@ -1882,194 +2314,195 @@ export class V2E2EECoordinator {
1882
2314
  payload,
1883
2315
  encrypted: false,
1884
2316
  };
2317
+ if (msg.sender_device_id !== undefined)
2318
+ v1Msg.sender_device_id = msg.sender_device_id;
1885
2319
  v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
1886
2320
  attachGatewayProximity(v1Msg, msg);
1887
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
1888
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
1889
- }
1890
- else {
1891
- client._markPublishedSeq(ns, seq);
1892
- }
2321
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
1893
2322
  decrypted.push(v1Msg);
1894
2323
  continue;
1895
2324
  }
2325
+ if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(String(payloadObj.type ?? '').trim())) {
2326
+ const deferStatus = {};
2327
+ const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
2328
+ if (deferStatus.deferred) {
2329
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2330
+ if (deferStatus.fromAid) {
2331
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2332
+ deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
2333
+ }
2334
+ }
2335
+ if (plaintext) {
2336
+ plaintext.group_id = gid;
2337
+ plaintext.group_aid = eventGroupAid;
2338
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2339
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2340
+ }
2341
+ else {
2342
+ client._markPublishedSeq(ns, seq);
2343
+ }
2344
+ decrypted.push(plaintext);
2345
+ }
2346
+ }
2347
+ client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
2348
+ continue;
1896
2349
  }
1897
- else if (payload !== undefined && payload !== null) {
1898
- let v1Msg = {
1899
- message_id: String(msg.message_id ?? ''),
1900
- from: String(msg.from_aid ?? ''),
1901
- group_id: gid,
1902
- group_aid: eventGroupAid,
1903
- seq: msg.seq,
1904
- type: String(msg.type ?? ''),
1905
- timestamp: msg.t_server,
1906
- payload,
1907
- encrypted: false,
1908
- };
1909
- v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
1910
- attachGatewayProximity(v1Msg, msg);
1911
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
1912
- decrypted.push(v1Msg);
2350
+ if (version !== 'v2') {
2351
+ client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
1913
2352
  continue;
1914
2353
  }
1915
- if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(String(payloadObj.type ?? '').trim())) {
1916
- const deferStatus = {};
1917
- const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
1918
- if (deferStatus.deferred) {
1919
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1920
- if (deferStatus.fromAid) {
1921
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1922
- deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
2354
+ const deferStatus = {};
2355
+ let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
2356
+ if (deferStatus.deferred) {
2357
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2358
+ }
2359
+ if (deferStatus.deferred && deferStatus.fromAid) {
2360
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2361
+ deferredKeyFetches.set(key, {
2362
+ fromAid: deferStatus.fromAid,
2363
+ senderDeviceId: deferStatus.senderDeviceId ?? '',
2364
+ groupId: deferStatus.groupId ?? '',
2365
+ });
2366
+ }
2367
+ if (plaintext === null)
2368
+ continue;
2369
+ plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
2370
+ plaintext.group_id = gid;
2371
+ plaintext.group_aid = eventGroupAid;
2372
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2373
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2374
+ }
2375
+ else {
2376
+ client._markPublishedSeq(ns, seq);
2377
+ }
2378
+ decrypted.push(plaintext);
2379
+ }
2380
+ const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
2381
+ const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
2382
+ const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
2383
+ const retentionFloor = Math.max(Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0), Number(cursor?.retention_floor_seq ?? 0));
2384
+ const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
2385
+ const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
2386
+ const commitTarget = Math.max(pageContigBefore, Number.isFinite(effectiveFloor) ? effectiveFloor : 0, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, cursorCurrentSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
2387
+ let ackSeq = pageContigBefore;
2388
+ let contigAdvanced = false;
2389
+ if (ownsCursor) {
2390
+ try {
2391
+ if (commitTarget > pageContigBefore) {
2392
+ client._seqTracker.forceContiguousSeq(ns, commitTarget);
2393
+ if (effectiveFloor > pageContigBefore) {
2394
+ client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
1923
2395
  }
1924
- }
1925
- if (plaintext) {
1926
- plaintext.group_id = gid;
1927
- plaintext.group_aid = eventGroupAid;
1928
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
1929
- await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2396
+ if (deferredServerCursor > pageContigBefore && pageCount === 1) {
2397
+ client._clientLog.info(`group.v2.pull restore local A from deferred server ACK cursor: group=${gid}, ns=${ns}, contiguous=${pageContigBefore} -> current_seq=${deferredServerCursor}`);
1930
2398
  }
1931
- else {
1932
- client._markPublishedSeq(ns, seq);
2399
+ if (seqs.length > 0) {
2400
+ client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
1933
2401
  }
1934
- decrypted.push(plaintext);
2402
+ }
2403
+ ackSeq = client._seqTracker.getContiguousSeq(ns);
2404
+ contigAdvanced = ackSeq !== pageContigBefore;
2405
+ if (contigAdvanced) {
2406
+ await client._commitSeqTrackerState(ns);
2407
+ if (pageCount === 1 && deferredServerCursor > 0)
2408
+ this.clearForwardCursor(ns, ackSeq);
1935
2409
  }
1936
2410
  }
1937
- client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
1938
- continue;
1939
- }
1940
- if (version !== 'v2') {
1941
- client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
1942
- continue;
1943
- }
1944
- const deferStatus = {};
1945
- let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
1946
- if (deferStatus.deferred) {
1947
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1948
- }
1949
- if (deferStatus.deferred && deferStatus.fromAid) {
1950
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1951
- deferredKeyFetches.set(key, {
1952
- fromAid: deferStatus.fromAid,
1953
- senderDeviceId: deferStatus.senderDeviceId ?? '',
1954
- groupId: deferStatus.groupId ?? '',
1955
- });
2411
+ catch (exc) {
2412
+ if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
2413
+ client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
2414
+ }
2415
+ client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
2416
+ throw exc;
2417
+ }
2418
+ if (contigAdvanced)
2419
+ await client._drainOrderedMessages(ns, undefined, false, false);
1956
2420
  }
1957
- if (plaintext === null)
1958
- continue;
1959
- plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
1960
- plaintext.group_id = gid;
1961
- plaintext.group_aid = eventGroupAid;
1962
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
1963
- await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2421
+ for (const fetch of deferredKeyFetches.values()) {
2422
+ this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1964
2423
  }
1965
- else {
1966
- client._markPublishedSeq(ns, seq);
2424
+ client._delivery.onPullPage?.(ns, decrypted.length - pageDecryptedBefore, hasMore, pageMaxSeq, typeof result.remaining === 'number' && Number.isSafeInteger(result.remaining) && result.remaining >= 0 ? result.remaining : null, messages.length, client._seqTracker.getContiguousSeq(ns), pageMaxSeq);
2425
+ if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
2426
+ this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
1967
2427
  }
1968
- decrypted.push(plaintext);
1969
- }
1970
- const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
1971
- const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
1972
- const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
1973
- const retentionFloor = Math.max(Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0), Number(cursor?.retention_floor_seq ?? 0));
1974
- const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
1975
- const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
1976
- const commitTarget = Math.max(pageContigBefore, Number.isFinite(effectiveFloor) ? effectiveFloor : 0, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, cursorCurrentSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
1977
- let ackSeq = pageContigBefore;
1978
- let contigAdvanced = false;
1979
- if (ownsCursor) {
1980
- try {
1981
- if (commitTarget > pageContigBefore) {
1982
- client._seqTracker.forceContiguousSeq(ns, commitTarget);
1983
- if (effectiveFloor > pageContigBefore) {
1984
- client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
1985
- }
1986
- if (deferredServerCursor > pageContigBefore && pageCount === 1) {
1987
- client._clientLog.info(`group.v2.pull restore local A from deferred server ACK cursor: group=${gid}, ns=${ns}, contiguous=${pageContigBefore} -> current_seq=${deferredServerCursor}`);
1988
- }
1989
- if (seqs.length > 0) {
1990
- client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
1991
- }
1992
- }
1993
- ackSeq = client._seqTracker.getContiguousSeq(ns);
1994
- contigAdvanced = ackSeq !== pageContigBefore;
1995
- if (contigAdvanced) {
1996
- await client._commitSeqTrackerState(ns);
1997
- if (pageCount === 1 && deferredServerCursor > 0)
1998
- this.clearForwardCursor(ns, ackSeq);
2428
+ // 无本页 cursor.current_seq 时,只有本页成功提交了比已知 cursor
2429
+ // 更高的 A 才新增 ACK 义务;空页不得因已有本地 A 重复确认。
2430
+ const knownServerCursorSeq = hasServerCursor
2431
+ ? cursorCurrentSeq
2432
+ : (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
2433
+ const ackNeeded = ackSeq > 0
2434
+ && ackSeq > lastAutoAckSeq
2435
+ && ownsCursor
2436
+ && ((hasServerCursor && ackSeq > cursorCurrentSeq)
2437
+ || (contigAdvanced && ackSeq > knownServerCursorSeq));
2438
+ if (ackNeeded) {
2439
+ this.recordForwardAck(ns, ackSeq);
2440
+ const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
2441
+ const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
2442
+ if (canContinuePage && !pipelineRunning) {
2443
+ pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
2444
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1999
2445
  }
2000
- }
2001
- catch (exc) {
2002
- if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
2003
- client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
2446
+ else {
2447
+ await this.confirmForwardGroupAck(gid, ackSeq, groupAid);
2448
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
2004
2449
  }
2005
- client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
2006
- throw exc;
2007
2450
  }
2008
- if (contigAdvanced)
2009
- await client._drainOrderedMessages(ns, undefined, false, false);
2010
- }
2011
- for (const fetch of deferredKeyFetches.values()) {
2012
- this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
2013
- }
2014
- client._delivery.onPullPage?.(ns, decrypted.length - pageDecryptedBefore, hasMore, pageMaxSeq, typeof result.remaining === 'number' && Number.isSafeInteger(result.remaining) && result.remaining >= 0 ? result.remaining : null, messages.length, client._seqTracker.getContiguousSeq(ns), pageMaxSeq);
2015
- if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
2016
- this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
2017
- }
2018
- // 无本页 cursor.current_seq 时,只有本页成功提交了比已知 cursor
2019
- // 更高的 A 才新增 ACK 义务;空页不得因已有本地 A 重复确认。
2020
- const knownServerCursorSeq = hasServerCursor
2021
- ? cursorCurrentSeq
2022
- : (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
2023
- const ackNeeded = ackSeq > 0
2024
- && ackSeq > lastAutoAckSeq
2025
- && ownsCursor
2026
- && ((hasServerCursor && ackSeq > cursorCurrentSeq)
2027
- || (contigAdvanced && ackSeq > knownServerCursorSeq));
2028
- if (ackNeeded) {
2029
- this.recordForwardAck(ns, ackSeq);
2030
2451
  const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
2031
- const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
2032
- if (canContinuePage) {
2033
- pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
2034
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
2452
+ if (!ownsCursor)
2453
+ break;
2454
+ const rawCount = messages.length;
2455
+ if (blockedSeq > 0)
2456
+ break;
2457
+ const shouldContinue = shouldContinueForwardPage({
2458
+ rawCount,
2459
+ nextAfterSeq,
2460
+ nextAfter,
2461
+ limit,
2462
+ hasMoreField,
2463
+ hasMore,
2464
+ serverHead,
2465
+ pendingAckSeq,
2466
+ observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
2467
+ ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
2468
+ });
2469
+ const shouldContinueForPipeline = shouldContinueForwardPage({
2470
+ rawCount,
2471
+ nextAfterSeq,
2472
+ nextAfter,
2473
+ limit,
2474
+ hasMoreField,
2475
+ hasMore,
2476
+ serverHead,
2477
+ pendingAckSeq: 0,
2478
+ observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
2479
+ ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
2480
+ });
2481
+ if (!shouldContinue)
2482
+ break;
2483
+ if (shouldContinueForPipeline && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
2484
+ if (pendingAckSeq > 0) {
2485
+ await this.confirmForwardGroupAck(gid, pendingAckSeq, groupAid);
2486
+ pendingAckSeq = 0;
2487
+ }
2488
+ startPipeline(nextAfter);
2035
2489
  }
2036
- else {
2037
- await this.confirmForwardGroupAck(gid, ackSeq, groupAid);
2038
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
2490
+ if (!pipelineRunning && pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
2491
+ const nextKey = pullGateKeyForClient(client, 'group.v2.pull', {
2492
+ group_id: gid, after_seq: nextAfter, force: opts?.force === true, limit,
2493
+ _group_cursor_params: cursorParams,
2494
+ }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
2495
+ await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
2496
+ pullGateKey = nextKey;
2039
2497
  }
2498
+ const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
2499
+ if (tailDelayMs > 0)
2500
+ await v2Sleep(tailDelayMs);
2501
+ nextAfterSeq = nextAfter;
2040
2502
  }
2041
- const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
2042
- if (!ownsCursor)
2043
- break;
2044
- const rawCount = messages.length;
2045
- if (blockedSeq > 0)
2046
- break;
2047
- const shouldContinue = shouldContinueForwardPage({
2048
- rawCount,
2049
- nextAfterSeq,
2050
- nextAfter,
2051
- limit,
2052
- hasMoreField,
2053
- hasMore,
2054
- serverHead,
2055
- pendingAckSeq,
2056
- observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
2057
- ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
2058
- });
2059
- if (!shouldContinue)
2060
- break;
2061
- if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
2062
- const nextKey = pullGateKeyForClient(client, 'group.v2.pull', {
2063
- group_id: gid, after_seq: nextAfter, force: opts?.force === true, limit,
2064
- _group_cursor_params: cursorParams,
2065
- }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
2066
- await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
2067
- pullGateKey = nextKey;
2068
- }
2069
- const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
2070
- if (tailDelayMs > 0)
2071
- await v2Sleep(tailDelayMs);
2072
- nextAfterSeq = nextAfter;
2503
+ }
2504
+ finally {
2505
+ await stopPipeline();
2073
2506
  }
2074
2507
  if (pageCount >= maxPages) {
2075
2508
  client._clientLog.warn(`group.v2.pull reached max_pages=${maxPages} group=${gid} after_seq=${nextAfterSeq}`);
@@ -2546,7 +2979,6 @@ export class V2E2EECoordinator {
2546
2979
  seq: msg.seq ?? null,
2547
2980
  timestamp: msg.timestamp ?? msg.t_server ?? null,
2548
2981
  device_id: msg.device_id ?? null,
2549
- slot_id: msg.slot_id ?? null,
2550
2982
  _decrypt_error: 'encrypted push payload is not decryptable on raw push path',
2551
2983
  _decrypt_stage: 'push_envelope',
2552
2984
  };
@@ -2590,13 +3022,18 @@ export class V2E2EECoordinator {
2590
3022
  encrypted: true,
2591
3023
  e2ee: e2eeMeta,
2592
3024
  };
2593
- result.direction = fromAid && fromAid === client._aid ? 'outbound_sync' : 'inbound';
3025
+ const senderDeviceId = String(aad.from_device ?? '').trim();
3026
+ if (senderDeviceId)
3027
+ result.sender_device_id = senderDeviceId;
3028
+ const direction = String(msg.direction ?? '').trim();
3029
+ if (direction)
3030
+ result.direction = direction;
3031
+ else if (fromAid && fromAid !== client._aid)
3032
+ result.direction = 'inbound';
2594
3033
  if (msg.t_server !== undefined)
2595
3034
  result.t_server = msg.t_server;
2596
3035
  if (msg.device_id !== undefined)
2597
3036
  result.device_id = msg.device_id;
2598
- if (msg.slot_id !== undefined)
2599
- result.slot_id = msg.slot_id;
2600
3037
  attachGatewayProximity(result, msg);
2601
3038
  if (group) {
2602
3039
  result.group_id = msg.group_id ?? aad.group_id ?? envelope.group_id ?? null;
@@ -2674,6 +3111,10 @@ export class V2E2EECoordinator {
2674
3111
  if (peerDevices.length === 0) {
2675
3112
  throw new E2EEError(`V2 bootstrap: no devices found for ${to}`);
2676
3113
  }
3114
+ const targetDeviceId = String(opts.deviceId ?? '').trim();
3115
+ if (targetDeviceId && !peerDevices.some((dev) => String(dev.device_id ?? '').trim() === targetDeviceId)) {
3116
+ throw new E2EEError(`V2 bootstrap: target device ${targetDeviceId} not found for ${to}`);
3117
+ }
2677
3118
  let targetSet = useCache ? this.getTargetSetCache(to) : undefined;
2678
3119
  if (targetSet) {
2679
3120
  targetSet = this.cloneP2PTargetSet(targetSet);
@@ -2751,6 +3192,20 @@ export class V2E2EECoordinator {
2751
3192
  });
2752
3193
  targetSet = this.cloneP2PTargetSet(targetSet);
2753
3194
  }
3195
+ if (targetDeviceId) {
3196
+ let peerTargetFound = false;
3197
+ targetSet.targets = targetSet.targets.filter((target) => {
3198
+ const isPeerTarget = target.aid === to
3199
+ && (target.deviceId === targetDeviceId || (wrapPolicy?.scope === 'aid' && !target.deviceId))
3200
+ && target.role !== 'self_sync';
3201
+ if (isPeerTarget)
3202
+ peerTargetFound = true;
3203
+ return isPeerTarget || target.role === 'self_sync';
3204
+ });
3205
+ if (!peerTargetFound) {
3206
+ throw new E2EEError(`V2 bootstrap: target device ${targetDeviceId} not found for ${to}`);
3207
+ }
3208
+ }
2754
3209
  const sender = await session.getSenderIdentity();
2755
3210
  const envelope = await encryptP2PMessage(sender, targetSet, opts.payload, { messageId: opts.messageId, timestamp: opts.timestamp, protectedHeaders: opts.protectedHeaders, context: opts.context });
2756
3211
  return envelope;