@agentunion/fastaun 0.5.33 → 0.5.35

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 (113) hide show
  1. package/CHANGELOG.md +59 -2
  2. package/README.md +28 -26
  3. package/_packed_docs/CHANGELOG.md +59 -2
  4. package/_packed_docs/INDEX.md +26 -12
  5. package/_packed_docs/KITE_DOCS_GUIDE.md +17 -11
  6. package/_packed_docs/agent.md/SCHEMA.md +1 -1
  7. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +2 -2
  8. package/_packed_docs/protocol/03-Gateway-/350/277/236/346/216/245/346/250/241/345/274/217.md +58 -8
  9. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +17 -17
  10. package/_packed_docs/protocol/echo-probe-gateway.md +99 -0
  11. package/_packed_docs/protocol/echo-probe-sdk.md +91 -0
  12. package/_packed_docs/sdk/0.5.33-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +464 -316
  13. package/_packed_docs/sdk/0.5.33-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +499 -304
  14. package/_packed_docs/sdk/0.5.34-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +133 -0
  15. package/_packed_docs/sdk/0.5.34-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +107 -0
  16. package/_packed_docs/sdk/0.5.35-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +189 -0
  17. package/_packed_docs/sdk/0.5.35-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +204 -0
  18. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +16 -13
  19. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +13 -1
  20. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +19 -9
  21. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +87 -20
  22. package/_packed_docs/sdk/09-collab-rpc-manual.md +48 -30
  23. package/_packed_docs/sdk/09-custody-api-manual.md +3 -3
  24. package/_packed_docs/sdk/09-group-rpc-manual.md +60 -13
  25. package/_packed_docs/sdk/09-message-rpc-manual.md +22 -0
  26. package/_packed_docs/sdk/09-meta-rpc-manual.md +2 -1
  27. package/_packed_docs/sdk/09-payload-reference.md +9 -8
  28. package/_packed_docs/sdk/09-proxy-rpc-manual.md +20 -14
  29. package/_packed_docs/sdk/09-storage-rpc-manual.md +1698 -1659
  30. package/_packed_docs/sdk/09-stream-rpc-manual.md +33 -20
  31. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +19 -17
  32. package/_packed_docs/sdk/AUN/344/272/213/344/273/266/345/217/221/345/270/203/350/256/242/351/230/205/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +125 -30
  33. package/_packed_docs/sdk/AUN/344/272/213/344/273/266/345/217/221/345/270/203/350/256/242/351/230/205/345/274/200/345/217/221/347/247/273/346/244/215/346/214/207/345/215/227.md +109 -33
  34. package/_packed_docs/sdk/INDEX.md +30 -18
  35. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +1 -1
  36. 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 +5 -5
  37. 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 +5 -5
  38. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +9 -9
  39. package/_packed_docs/sdk/README.md +11 -6
  40. package/_packed_docs//346/266/210/346/201/257/345/274/271/346/200/247/350/241/245/346/213/211/351/200/232/351/201/223/345/256/236/346/226/275/350/256/241/345/210/222.md +400 -0
  41. package/_packed_docs//346/266/210/346/201/257/345/274/271/346/200/247/350/241/245/346/213/211/351/200/232/351/201/223/346/226/271/346/241/210.md +485 -0
  42. package/dist/auth.js +39 -45
  43. package/dist/auth.js.map +1 -1
  44. package/dist/client/control-messages.js +2 -2
  45. package/dist/client/control-messages.js.map +1 -1
  46. package/dist/client/delivery.d.ts +12 -1
  47. package/dist/client/delivery.js +281 -46
  48. package/dist/client/delivery.js.map +1 -1
  49. package/dist/client/lifecycle.js +13 -1
  50. package/dist/client/lifecycle.js.map +1 -1
  51. package/dist/client/rpc-budget.d.ts +21 -0
  52. package/dist/client/rpc-budget.js +82 -0
  53. package/dist/client/rpc-budget.js.map +1 -0
  54. package/dist/client/rpc-pipeline.d.ts +5 -0
  55. package/dist/client/rpc-pipeline.js +227 -63
  56. package/dist/client/rpc-pipeline.js.map +1 -1
  57. package/dist/client/sync-channels.d.ts +90 -0
  58. package/dist/client/sync-channels.js +508 -0
  59. package/dist/client/sync-channels.js.map +1 -0
  60. package/dist/client/v2-e2ee.d.ts +5 -1
  61. package/dist/client/v2-e2ee.js +420 -303
  62. package/dist/client/v2-e2ee.js.map +1 -1
  63. package/dist/client.d.ts +17 -0
  64. package/dist/client.js +373 -50
  65. package/dist/client.js.map +1 -1
  66. package/dist/events.d.ts +1 -0
  67. package/dist/events.js +5 -0
  68. package/dist/events.js.map +1 -1
  69. package/dist/index.d.ts +3 -1
  70. package/dist/index.js +1 -0
  71. package/dist/index.js.map +1 -1
  72. package/dist/mail-client.d.ts +101 -0
  73. package/dist/mail-client.js +369 -0
  74. package/dist/mail-client.js.map +1 -0
  75. package/dist/pubsub/binding.js +23 -3
  76. package/dist/pubsub/binding.js.map +1 -1
  77. package/dist/pubsub/channel.d.ts +15 -3
  78. package/dist/pubsub/channel.js +114 -7
  79. package/dist/pubsub/channel.js.map +1 -1
  80. package/dist/pubsub/client.d.ts +17 -2
  81. package/dist/pubsub/client.js +205 -40
  82. package/dist/pubsub/client.js.map +1 -1
  83. package/dist/pubsub/credentials.d.ts +3 -0
  84. package/dist/pubsub/credentials.js +57 -20
  85. package/dist/pubsub/credentials.js.map +1 -1
  86. package/dist/pubsub/index.d.ts +5 -2
  87. package/dist/pubsub/index.js +70 -54
  88. package/dist/pubsub/index.js.map +1 -1
  89. package/dist/pubsub/service.d.ts +9 -0
  90. package/dist/pubsub/service.js +266 -20
  91. package/dist/pubsub/service.js.map +1 -1
  92. package/dist/pubsub/types.d.ts +13 -1
  93. package/dist/storage/lowlevel.d.ts +3 -1
  94. package/dist/storage/lowlevel.js +13 -0
  95. package/dist/storage/lowlevel.js.map +1 -1
  96. package/dist/storage/types.d.ts +10 -0
  97. package/dist/storage/types.js.map +1 -1
  98. package/dist/storage/vfs.d.ts +3 -1
  99. package/dist/storage/vfs.js +2 -2
  100. package/dist/storage/vfs.js.map +1 -1
  101. package/dist/tools/cross-sdk-agent.d.ts +55 -5
  102. package/dist/tools/cross-sdk-agent.js +88 -21
  103. package/dist/tools/cross-sdk-agent.js.map +1 -1
  104. package/dist/transport.d.ts +16 -0
  105. package/dist/transport.js +129 -7
  106. package/dist/transport.js.map +1 -1
  107. package/dist/udp-standby.d.ts +89 -0
  108. package/dist/udp-standby.js +487 -0
  109. package/dist/udp-standby.js.map +1 -0
  110. package/dist/version.d.ts +1 -1
  111. package/dist/version.js +1 -1
  112. package/package.json +5 -1
  113. package/scripts/patch-imapflow-external.mjs +69 -0
@@ -73,10 +73,10 @@ function strictWindowSeq(value, field, positive = false) {
73
73
  function safeNonNegativeSeq(value) {
74
74
  return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
75
75
  }
76
- function strictWindowLimit(value) {
76
+ function strictWindowLimit(value, maxLimit = V2_PULL_MAX_PAGE_LIMIT) {
77
77
  const limit = strictWindowSeq(value, 'limit', true);
78
- if (limit > V2_PULL_MAX_PAGE_LIMIT)
79
- throw new ValidationError(`limit must be <= ${V2_PULL_MAX_PAGE_LIMIT}`);
78
+ if (limit > maxLimit)
79
+ throw new ValidationError(`limit must be <= ${maxLimit}`);
80
80
  return limit;
81
81
  }
82
82
  function deduplicateWindowMessages(value, lowerSeq, upperSeq, boundary) {
@@ -545,6 +545,7 @@ export class V2E2EECoordinator {
545
545
  if (identityChanged())
546
546
  return;
547
547
  this.runtime.v2.setSessionState(v2Store, session);
548
+ client._delivery.requestBackgroundPullRecovery();
548
549
  client._clientLog.debug(`V2 session initialized aid=${aidAtStart} device=${deviceIdAtStart}`);
549
550
  }
550
551
  scheduleGroupSpkRegistration(groupId, opts) {
@@ -1316,12 +1317,91 @@ export class V2E2EECoordinator {
1316
1317
  }
1317
1318
  return plaintext;
1318
1319
  }
1320
+ async pullPeerV2Volatile(senderAid, opts = {}) {
1321
+ const client = this.client;
1322
+ const generation = Number(client._v2RuntimeGeneration ?? 0);
1323
+ const inlineGeneration = client._delivery.captureInlineAckGeneration?.();
1324
+ const ns = client._delivery.peerNamespace(senderAid);
1325
+ const limit = Number(opts.limit ?? _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize);
1326
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200)
1327
+ throw new ValidationError('peer limit must be between 1 and 200');
1328
+ const maxPages = Math.max(1, Math.min(100, Number(opts.maxPages ?? 100) || 100));
1329
+ const maxPageBytes = client._syncChannelCoordinator?.volatilePageBytes?.() ?? 262144;
1330
+ await client._ensureV2SessionReady('message.pull');
1331
+ return await client._delivery.runPeerPageSerialized(`${ns}|volatile`, async () => {
1332
+ const messages = [];
1333
+ let pages = 0, rawCount = 0, hasMore = false;
1334
+ while (pages < maxPages) {
1335
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1336
+ if (opts.background && !client._delivery.hasP2pApplicationSubscription())
1337
+ break;
1338
+ let cursor = client._delivery.peerVolatilePullCursor(senderAid);
1339
+ const request = () => client._callRawV2Rpc('message.v2.pull', {
1340
+ cursor_scope: 'peer', sender_aid: senderAid, device_id: client._deviceId, slot_id: client._slotId,
1341
+ storage_scope: 'volatile', limit, max_page_bytes: maxPageBytes,
1342
+ ...(cursor ? { after_volatile_cursor: cursor } : {}),
1343
+ ...(opts.background ? { _rpc_background: true } : { _rpc_foreground: true }),
1344
+ });
1345
+ const reservation = await client._dispatcher?._reserveMessageCapacity?.(limit);
1346
+ try {
1347
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1348
+ let response;
1349
+ try {
1350
+ response = await request();
1351
+ }
1352
+ catch (error) {
1353
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1354
+ if (!cursor || !String(error instanceof Error ? error.message : error).toLowerCase().includes('volatile cursor epoch mismatch'))
1355
+ throw error;
1356
+ client._delivery.setPeerVolatilePullCursor(senderAid, '');
1357
+ cursor = '';
1358
+ response = await request();
1359
+ }
1360
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1361
+ if (!response || response.storage_scope !== 'volatile' || !Array.isArray(response.messages)
1362
+ || response.messages.length > limit || typeof response.volatile_has_more !== 'boolean'
1363
+ || response.has_more !== response.volatile_has_more || typeof response.next_volatile_cursor !== 'string'
1364
+ || new TextEncoder().encode(JSON.stringify(response)).byteLength > maxPageBytes)
1365
+ throw new ValidationError('invalid volatile peer page');
1366
+ const rows = response.messages;
1367
+ for (const row of rows) {
1368
+ if (!isJsonObject(row) || row.storage !== 'volatile' || String(row.sender_aid ?? row.from_aid ?? row.from ?? '') !== senderAid)
1369
+ throw new ValidationError('volatile peer sender mismatch');
1370
+ }
1371
+ const handlers = client._delivery.snapshotApplicationHandlers?.();
1372
+ await this.withApplicationPage(ns, 'pull', async () => {
1373
+ const delivered = await this.publishV2VolatileMessages(rows, 'pull', true);
1374
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1375
+ if (opts.retainMessages !== false)
1376
+ messages.push(...delivered);
1377
+ }, reservation, handlers);
1378
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1379
+ if (!rows.every(row => client._delivery.isVolatileSeen(row)))
1380
+ throw new E2EEError('volatile peer delivery remains pending');
1381
+ client._delivery.setPeerVolatilePullCursor(senderAid, response.next_volatile_cursor);
1382
+ pages++;
1383
+ rawCount += rows.length;
1384
+ hasMore = response.volatile_has_more;
1385
+ if (!hasMore)
1386
+ break;
1387
+ if (response.next_volatile_cursor === cursor)
1388
+ throw new E2EEError('volatile peer cursor made no progress');
1389
+ }
1390
+ finally {
1391
+ reservation?.release();
1392
+ }
1393
+ }
1394
+ return { sender_aid: senderAid, messages, complete: !hasMore, pages, raw_count: rawCount, volatile_has_more: hasMore };
1395
+ });
1396
+ }
1319
1397
  async pullPeerV2(senderAid, opts = {}) {
1320
1398
  const client = this.client;
1321
1399
  const generation = Number(client._v2RuntimeGeneration ?? 0);
1322
1400
  const inlineGeneration = client._delivery.captureInlineAckGeneration?.();
1323
1401
  const pullVersion = opts.peerPullVersion ?? client._delivery.capturePeerTailVersion(senderAid);
1324
1402
  await client._ensureV2SessionReady('message.pull');
1403
+ const volatile = opts.background && opts.window_mode !== 'tail' && client._syncChannelCoordinator?.needsSplit?.()
1404
+ ? await this.pullPeerV2Volatile(senderAid, { background: true, limit: opts.limit, retainMessages: opts.retainMessages }) : null;
1325
1405
  this.ensureDeliveryCurrent(client._delivery.peerNamespace(senderAid), generation, inlineGeneration);
1326
1406
  const ns = await client._delivery.initializePeer(senderAid);
1327
1407
  this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
@@ -1337,14 +1417,14 @@ export class V2E2EECoordinator {
1337
1417
  if (mode !== 'tail' && opts.force !== true && after > client._seqTracker.getContiguousSeq(ns))
1338
1418
  throw new ValidationError('after_seq cannot exceed the peer delivery cursor');
1339
1419
  let volatileCursor = String(opts.after_volatile_cursor ?? client._delivery.peerVolatilePullCursor(senderAid));
1340
- const decoded = [];
1420
+ const decoded = volatile?.messages ?? [];
1341
1421
  let complete = false;
1342
1422
  let head = after;
1343
1423
  let tailVolatileHasMore;
1344
1424
  let tailServerAck = null;
1345
1425
  let pages = 0;
1346
- let rawCount = 0;
1347
- let forwardVolatileHasMore = false;
1426
+ let rawCount = volatile?.raw_count ?? 0;
1427
+ let forwardVolatileHasMore = volatile?.volatile_has_more ?? false;
1348
1428
  let durableAck = 0;
1349
1429
  let initializedAck = false;
1350
1430
  let confirmedAck = 0;
@@ -1380,7 +1460,7 @@ export class V2E2EECoordinator {
1380
1460
  else {
1381
1461
  start = strictWindowSeq(response.scan_after_seq, 'scan_after_seq');
1382
1462
  const retention = safeNonNegativeSeq(response.retention_floor_seq) ?? 0;
1383
- if (start < requestAfter || (!preparing && start > Math.max(requestAfter, retention, opts.force === true ? 0 : committedAck))
1463
+ if (start < Math.max(requestAfter, retention) || (!preparing && start > Math.max(requestAfter, retention, opts.force === true ? 0 : committedAck))
1384
1464
  || end < start || end > pageHead || response.next_after_seq !== end)
1385
1465
  throw new ValidationError('invalid peer Forward proof');
1386
1466
  if (typeof response.persistent_has_more !== 'boolean' || typeof response.volatile_has_more !== 'boolean'
@@ -1492,6 +1572,7 @@ export class V2E2EECoordinator {
1492
1572
  }
1493
1573
  catch (error) {
1494
1574
  fetched.reservation?.release();
1575
+ client._syncChannelCoordinator?.releaseResponse(fetched.response);
1495
1576
  throw error;
1496
1577
  }
1497
1578
  });
@@ -1606,13 +1687,14 @@ export class V2E2EECoordinator {
1606
1687
  }
1607
1688
  finally {
1608
1689
  reservation?.release();
1690
+ client._syncChannelCoordinator?.releaseResponse(response);
1609
1691
  }
1610
1692
  });
1611
1693
  if (!page)
1612
1694
  break;
1613
1695
  this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1614
1696
  rawCount += page.response.messages.length;
1615
- forwardVolatileHasMore = page.response.volatile_has_more === true;
1697
+ forwardVolatileHasMore = volatile ? volatile.volatile_has_more === true : page.response.volatile_has_more === true;
1616
1698
  if (opts.retainMessages !== false)
1617
1699
  decoded.push(...page.messages);
1618
1700
  head = page.head;
@@ -1624,7 +1706,7 @@ export class V2E2EECoordinator {
1624
1706
  }
1625
1707
  volatileCursor = client._delivery.peerVolatilePullCursor(senderAid);
1626
1708
  const more = page.response.persistent_has_more === true || page.response.volatile_has_more === true || page.response.has_more === true;
1627
- complete = !more;
1709
+ complete = !more && !forwardVolatileHasMore;
1628
1710
  client._delivery.syncProgress(ns, { rawCount: page.response.messages.length, pulledCount: page.messages.length, hasMore: more,
1629
1711
  currentSeq: client._seqTracker.getContiguousSeq(ns), targetSeq: head });
1630
1712
  after = page.end;
@@ -1640,7 +1722,9 @@ export class V2E2EECoordinator {
1640
1722
  stopping.abort();
1641
1723
  for (const pending of prefetched) {
1642
1724
  try {
1643
- (await (pending.prepared ?? pending.response))?.reservation?.release();
1725
+ const fetched = await (pending.prepared ?? pending.response);
1726
+ fetched?.reservation?.release();
1727
+ client._syncChannelCoordinator?.releaseResponse(fetched?.response);
1644
1728
  }
1645
1729
  catch { }
1646
1730
  }
@@ -1649,9 +1733,16 @@ export class V2E2EECoordinator {
1649
1733
  this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1650
1734
  const pending = this.pendingForwardAck(ns, durableAck);
1651
1735
  if (pending > 0) {
1652
- await this.ackPeerV2(senderAid, pending);
1653
- this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1654
- confirmAck(pending);
1736
+ const confirm = async (seq) => {
1737
+ await this.ackPeerV2(senderAid, seq, [], [], Boolean(opts.background));
1738
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1739
+ confirmAck(seq);
1740
+ this.clearForwardAck(ns, seq);
1741
+ };
1742
+ if (opts.background)
1743
+ client._delivery.scheduleForwardAck(ns, pending, confirm);
1744
+ else
1745
+ await confirm(pending);
1655
1746
  }
1656
1747
  }
1657
1748
  catch (error) {
@@ -1681,7 +1772,7 @@ export class V2E2EECoordinator {
1681
1772
  || (response.slot_id !== undefined && response.slot_id !== client._slotId))
1682
1773
  throw new ValidationError('peer response scope mismatch');
1683
1774
  }
1684
- async ackPeerV2(senderAid, upToSeq, volatileCursors = [], volatileIds = []) {
1775
+ async ackPeerV2(senderAid, upToSeq, volatileCursors = [], volatileIds = [], background = true) {
1685
1776
  const client = this.client;
1686
1777
  const generation = Number(client._v2RuntimeGeneration ?? 0);
1687
1778
  const inlineGeneration = client._delivery.captureInlineAckGeneration?.();
@@ -1696,7 +1787,7 @@ export class V2E2EECoordinator {
1696
1787
  const result = await client._callRawV2Rpc('message.v2.ack', { cursor_scope: 'peer', sender_aid: senderAid,
1697
1788
  device_id: client._deviceId, slot_id: client._slotId,
1698
1789
  ...(seq ? { up_to_seq: seq } : {}), ...(volatileCursors.length ? { volatile_cursors: volatileCursors } : {}),
1699
- ...(volatileIds.length ? { volatile_ids: volatileIds } : {}), _rpc_background: true });
1790
+ ...(volatileIds.length ? { volatile_ids: volatileIds } : {}), _rpc_background: background, _rpc_foreground: !background });
1700
1791
  this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1701
1792
  this.validatePeerResponse(result, senderAid);
1702
1793
  const actual = safeNonNegativeSeq(result.peer_ack_seq ?? result.effective_ack_seq ?? result.up_to_seq);
@@ -2296,7 +2387,10 @@ export class V2E2EECoordinator {
2296
2387
  this.ensureDeliveryCurrent(ns, generation);
2297
2388
  client._clientLog.debug(`message.v2.pull flushing pending auto-ack: ns=${ns}, ack_seq=${pendingAckSeq}`);
2298
2389
  const ackToFlush = pendingAckSeq;
2299
- await this.confirmForwardP2PAck(ackToFlush);
2390
+ if (opts?.background)
2391
+ client._delivery.scheduleForwardAck(ns, ackToFlush, (seq) => this.confirmForwardP2PAck(seq));
2392
+ else
2393
+ await this.confirmForwardP2PAck(ackToFlush);
2300
2394
  if (pendingAckSeq <= ackToFlush)
2301
2395
  pendingAckSeq = 0;
2302
2396
  }
@@ -2710,7 +2804,7 @@ export class V2E2EECoordinator {
2710
2804
  throw new ValidationError('group_id is required');
2711
2805
  let groupAid = String(params.group_aid ?? '').trim();
2712
2806
  const afterSeq = strictWindowSeq(params.after_seq ?? 0, 'after_seq');
2713
- const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
2807
+ const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT, 200);
2714
2808
  const ns = `group:${groupId}`;
2715
2809
  const namespaceGeneration = client._delivery.captureSeqNamespaceGeneration?.(ns);
2716
2810
  const explicitCursorParams = isJsonObject(params._group_cursor_params)
@@ -2823,6 +2917,7 @@ export class V2E2EECoordinator {
2823
2917
  await this.pullGroupV2(params.group_id, params.after_seq, params.limit, {
2824
2918
  gateLocked: true,
2825
2919
  explicitAfterSeq: true,
2920
+ retainMessages: false,
2826
2921
  force: params.force === true,
2827
2922
  });
2828
2923
  }
@@ -2871,6 +2966,12 @@ export class V2E2EECoordinator {
2871
2966
  }), opts?.background !== false);
2872
2967
  }
2873
2968
  const decrypted = [];
2969
+ let decryptedCount = 0;
2970
+ const collectMessage = (message) => {
2971
+ decryptedCount += 1;
2972
+ if (opts?.retainMessages !== false)
2973
+ decrypted.push(message);
2974
+ };
2874
2975
  const wireGroupId = String(opts?.wireGroupId ?? groupId ?? '').trim() || gid;
2875
2976
  const cursorParams = opts?.cursorParams ?? {};
2876
2977
  const ownsCursor = opts?.ownsCursor !== false;
@@ -2886,18 +2987,21 @@ export class V2E2EECoordinator {
2886
2987
  let lastAutoAckSeq = 0;
2887
2988
  let pendingAckSeq = deferredForwardAck;
2888
2989
  let committedAckSeq = ownsCursor ? client._seqTracker.getContiguousSeq(ns) : 0;
2990
+ const syncRetainedPages = new Set();
2889
2991
  const fetchPage = async (after, ack) => {
2890
2992
  const key = pullGateKeyForClient(client, 'group.v2.pull', {
2891
2993
  group_id: gid, after_seq: after, force: opts?.force === true, limit, _group_cursor_params: cursorParams,
2892
2994
  }, `${ns}|forward|after=${after}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
2893
2995
  return await client._runPullSerialized(key, async () => {
2894
2996
  this.ensureDeliveryCurrent(ns, generation);
2895
- return await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2997
+ const response = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2896
2998
  group_id: wireGroupId, after_seq: after, limit, ...cursorParams,
2897
2999
  ...(opts?.force ? { force: true } : {}),
2898
3000
  ...(ack > 0 ? { ack_up_to_seq: Math.min(ack, committedAckSeq) } : {}),
2899
3001
  _rpc_background: background, _rpc_foreground: !background,
2900
3002
  }, groupAid), key);
3003
+ syncRetainedPages.add(response);
3004
+ return response;
2901
3005
  }, background);
2902
3006
  };
2903
3007
  const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
@@ -2998,92 +3102,121 @@ export class V2E2EECoordinator {
2998
3102
  else {
2999
3103
  result = await fetchPage(nextAfterSeq, ackUpToSeq);
3000
3104
  }
3001
- if (ownsCursor)
3002
- this.ensureDeliveryCurrent(ns, generation);
3003
- if (ackUpToSeq > 0) {
3004
- const actualAckSeq = client._delivery.resolveGroupPullAckSeq(result, ackUpToSeq);
3005
- if (actualAckSeq >= ackUpToSeq) {
3006
- if (inlineAckGeneration !== null) {
3007
- client._delivery.onGroupV2PullAckResult(gid, result, ackUpToSeq, inlineAckGeneration);
3105
+ try {
3106
+ if (ownsCursor)
3107
+ this.ensureDeliveryCurrent(ns, generation);
3108
+ if (ackUpToSeq > 0) {
3109
+ const actualAckSeq = client._delivery.resolveGroupPullAckSeq(result, ackUpToSeq);
3110
+ if (actualAckSeq >= ackUpToSeq) {
3111
+ if (inlineAckGeneration !== null) {
3112
+ client._delivery.onGroupV2PullAckResult(gid, result, ackUpToSeq, inlineAckGeneration);
3113
+ }
3114
+ if (pendingAckSeq <= ackUpToSeq)
3115
+ pendingAckSeq = 0;
3116
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
3117
+ this.clearForwardAck(ns, ackUpToSeq);
3118
+ }
3119
+ else {
3120
+ client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
3008
3121
  }
3009
- if (pendingAckSeq <= ackUpToSeq)
3010
- pendingAckSeq = 0;
3011
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
3012
- this.clearForwardAck(ns, ackUpToSeq);
3013
- }
3014
- else {
3015
- client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
3016
3122
  }
3017
- }
3018
- const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
3019
- if (resultGroupAid)
3020
- groupAid = resultGroupAid;
3021
- const eventGroupAid = resultGroupAid || groupAid || gid;
3022
- const pageMessages = (Array.isArray(result.messages) ? result.messages : []);
3023
- const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
3024
- const cursor = isJsonObject(result.cursor) ? result.cursor : null;
3025
- const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
3026
- const hasMore = result.has_more === true;
3027
- const serverHead = Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0);
3028
- client._clientLog.debug(`group.v2.pull page response: group=${gid}, page=${pageCount}, raw_count=${messages.length}, stale_count=${Math.max(0, pageMessages.length - messages.length)}, has_more=${String(result.has_more ?? '')}, server_head=${serverHead}, cursor_current=${String(cursor?.current_seq ?? '')}`);
3029
- for (const msg of messages) {
3030
- client._logMessageDebug('pull-raw', 'group.v2.pull', 'group.message_created', msg);
3031
- }
3032
- const seqs = messages
3033
- .map((msg) => Number(msg.seq ?? 0))
3034
- .filter((seq) => Number.isFinite(seq) && seq > 0);
3035
- const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
3036
- const namespaceGeneration = client._delivery.captureSeqNamespaceGeneration?.(ns);
3037
- let pageTrackerSnapshot = null;
3038
- let trackerMutated = false;
3039
- let pageMaxSeq = nextAfterSeq;
3040
- if (seqs.length > 0) {
3041
- pageMaxSeq = Math.max(...seqs);
3042
- }
3043
- const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
3044
- if (!pipelineRunning && ownsCursor && shouldContinueForwardPage({
3045
- allowEmptyCoverage: true,
3046
- rawCount: messages.length, nextAfterSeq, nextAfter: pageMaxSeq, limit,
3047
- hasMoreField, hasMore, serverHead,
3048
- observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._delivery.pendingPullUpper?.(ns) ?? 0)),
3049
- pendingAckSeq: 0,
3050
- }))
3051
- startPipeline(pageMaxSeq);
3052
- const deferredKeyFetches = new Map();
3053
- let blockedSeq = 0;
3054
- const pageDecryptedBefore = decrypted.length;
3055
- const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
3056
- const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
3057
- const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
3058
- const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq'));
3059
- const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
3060
- const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
3061
- let commitTarget = pageContigBefore;
3062
- await this.withApplicationPage(ns, 'pull', async () => {
3123
+ const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
3124
+ if (resultGroupAid)
3125
+ groupAid = resultGroupAid;
3126
+ const eventGroupAid = resultGroupAid || groupAid || gid;
3127
+ const pageMessages = (Array.isArray(result.messages) ? result.messages : []);
3128
+ const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
3129
+ const cursor = isJsonObject(result.cursor) ? result.cursor : null;
3130
+ const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
3131
+ const hasMore = result.has_more === true;
3132
+ const serverHead = Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0);
3133
+ client._clientLog.debug(`group.v2.pull page response: group=${gid}, page=${pageCount}, raw_count=${messages.length}, stale_count=${Math.max(0, pageMessages.length - messages.length)}, has_more=${String(result.has_more ?? '')}, server_head=${serverHead}, cursor_current=${String(cursor?.current_seq ?? '')}`);
3063
3134
  for (const msg of messages) {
3064
- const seq = Number(msg.seq ?? 0);
3065
- if (!Number.isFinite(seq) || seq <= 0)
3066
- continue;
3067
- const version = String(msg.version ?? 'v2');
3068
- if (version === 'v1') {
3069
- const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
3070
- const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
3071
- const payloadObj = isJsonObject(payload) ? payload : null;
3072
- const payloadType = payloadObj ? String(payloadObj.type ?? '').trim() : '';
3073
- // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
3074
- if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
3075
- && client._delivery.recallEventFromGroupMessage(msg)) {
3076
- await client._delivery.publishGroupRecallTombstone(gid, seq, {
3077
- ...msg,
3078
- group_id: gid,
3079
- group_aid: eventGroupAid,
3080
- }, 'pull');
3081
- client._markPublishedSeq(ns, seq);
3082
- client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
3135
+ client._logMessageDebug('pull-raw', 'group.v2.pull', 'group.message_created', msg);
3136
+ }
3137
+ const seqs = messages
3138
+ .map((msg) => Number(msg.seq ?? 0))
3139
+ .filter((seq) => Number.isFinite(seq) && seq > 0);
3140
+ const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
3141
+ const namespaceGeneration = client._delivery.captureSeqNamespaceGeneration?.(ns);
3142
+ let pageTrackerSnapshot = null;
3143
+ let trackerMutated = false;
3144
+ let pageMaxSeq = nextAfterSeq;
3145
+ if (seqs.length > 0) {
3146
+ pageMaxSeq = Math.max(...seqs);
3147
+ }
3148
+ const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
3149
+ if (!pipelineRunning && ownsCursor && shouldContinueForwardPage({
3150
+ allowEmptyCoverage: true,
3151
+ rawCount: messages.length, nextAfterSeq, nextAfter: pageMaxSeq, limit,
3152
+ hasMoreField, hasMore, serverHead,
3153
+ observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._delivery.pendingPullUpper?.(ns) ?? 0)),
3154
+ pendingAckSeq: 0,
3155
+ }))
3156
+ startPipeline(pageMaxSeq);
3157
+ const deferredKeyFetches = new Map();
3158
+ let blockedSeq = 0;
3159
+ const pageDecryptedBefore = decryptedCount;
3160
+ const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
3161
+ const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
3162
+ const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
3163
+ const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq'));
3164
+ const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
3165
+ const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
3166
+ let commitTarget = pageContigBefore;
3167
+ await this.withApplicationPage(ns, 'pull', async () => {
3168
+ for (const msg of messages) {
3169
+ const seq = Number(msg.seq ?? 0);
3170
+ if (!Number.isFinite(seq) || seq <= 0)
3083
3171
  continue;
3084
- }
3085
- if (payloadObj) {
3086
- if (!['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
3172
+ const version = String(msg.version ?? 'v2');
3173
+ if (version === 'v1') {
3174
+ const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
3175
+ const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
3176
+ const payloadObj = isJsonObject(payload) ? payload : null;
3177
+ const payloadType = payloadObj ? String(payloadObj.type ?? '').trim() : '';
3178
+ // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
3179
+ if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
3180
+ && client._delivery.recallEventFromGroupMessage(msg)) {
3181
+ await client._delivery.publishGroupRecallTombstone(gid, seq, {
3182
+ ...msg,
3183
+ group_id: gid,
3184
+ group_aid: eventGroupAid,
3185
+ }, 'pull');
3186
+ client._markPublishedSeq(ns, seq);
3187
+ client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
3188
+ continue;
3189
+ }
3190
+ if (payloadObj) {
3191
+ if (!['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
3192
+ let v1Msg = {
3193
+ message_id: String(msg.message_id ?? ''),
3194
+ from: String(msg.from_aid ?? ''),
3195
+ group_id: gid,
3196
+ group_aid: eventGroupAid,
3197
+ seq: msg.seq,
3198
+ type: String(msg.type ?? ''),
3199
+ timestamp: msg.t_server,
3200
+ payload,
3201
+ encrypted: false,
3202
+ need_render: msg.need_render !== false,
3203
+ };
3204
+ if (msg.sender_device_id !== undefined)
3205
+ v1Msg.sender_device_id = msg.sender_device_id;
3206
+ v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
3207
+ attachGatewayProximity(v1Msg, msg);
3208
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
3209
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
3210
+ }
3211
+ else {
3212
+ client._markPublishedSeq(ns, seq);
3213
+ }
3214
+ collectMessage(v1Msg);
3215
+ client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
3216
+ continue;
3217
+ }
3218
+ }
3219
+ else if (payload !== undefined && payload !== null) {
3087
3220
  let v1Msg = {
3088
3221
  message_id: String(msg.message_id ?? ''),
3089
3222
  from: String(msg.from_aid ?? ''),
@@ -3100,231 +3233,211 @@ export class V2E2EECoordinator {
3100
3233
  v1Msg.sender_device_id = msg.sender_device_id;
3101
3234
  v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
3102
3235
  attachGatewayProximity(v1Msg, msg);
3103
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
3104
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
3105
- }
3106
- else {
3107
- client._markPublishedSeq(ns, seq);
3108
- }
3109
- decrypted.push(v1Msg);
3236
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
3237
+ collectMessage(v1Msg);
3110
3238
  client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
3111
3239
  continue;
3112
3240
  }
3113
- }
3114
- else if (payload !== undefined && payload !== null) {
3115
- let v1Msg = {
3116
- message_id: String(msg.message_id ?? ''),
3117
- from: String(msg.from_aid ?? ''),
3118
- group_id: gid,
3119
- group_aid: eventGroupAid,
3120
- seq: msg.seq,
3121
- type: String(msg.type ?? ''),
3122
- timestamp: msg.t_server,
3123
- payload,
3124
- encrypted: false,
3125
- need_render: msg.need_render !== false,
3126
- };
3127
- if (msg.sender_device_id !== undefined)
3128
- v1Msg.sender_device_id = msg.sender_device_id;
3129
- v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
3130
- attachGatewayProximity(v1Msg, msg);
3131
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
3132
- decrypted.push(v1Msg);
3133
- client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
3134
- continue;
3135
- }
3136
- if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
3137
- const deferStatus = {};
3138
- const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
3139
- if (deferStatus.deferred) {
3140
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
3141
- if (deferStatus.fromAid) {
3142
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
3143
- deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
3144
- }
3145
- }
3146
- if (plaintext) {
3147
- plaintext.group_id = gid;
3148
- plaintext.group_aid = eventGroupAid;
3149
- plaintext.need_render = typeof msg.need_render === 'boolean' ? msg.need_render : plaintext.need_render !== false;
3150
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
3151
- await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
3241
+ if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
3242
+ const deferStatus = {};
3243
+ const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
3244
+ if (deferStatus.deferred) {
3245
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
3246
+ if (deferStatus.fromAid) {
3247
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
3248
+ deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
3249
+ }
3152
3250
  }
3153
- else {
3154
- client._markPublishedSeq(ns, seq);
3251
+ if (plaintext) {
3252
+ plaintext.group_id = gid;
3253
+ plaintext.group_aid = eventGroupAid;
3254
+ plaintext.need_render = typeof msg.need_render === 'boolean' ? msg.need_render : plaintext.need_render !== false;
3255
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
3256
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
3257
+ }
3258
+ else {
3259
+ client._markPublishedSeq(ns, seq);
3260
+ }
3261
+ collectMessage(plaintext);
3155
3262
  }
3156
- decrypted.push(plaintext);
3157
3263
  }
3264
+ else {
3265
+ client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
3266
+ }
3267
+ continue;
3158
3268
  }
3159
- else {
3160
- client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
3269
+ if (version !== 'v2') {
3270
+ client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
3271
+ continue;
3161
3272
  }
3162
- continue;
3163
- }
3164
- if (version !== 'v2') {
3165
- client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
3166
- continue;
3167
- }
3168
- const deferStatus = {};
3169
- let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
3170
- if (deferStatus.deferred) {
3171
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
3172
- }
3173
- if (deferStatus.deferred && deferStatus.fromAid) {
3174
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
3175
- deferredKeyFetches.set(key, {
3176
- fromAid: deferStatus.fromAid,
3177
- senderDeviceId: deferStatus.senderDeviceId ?? '',
3178
- groupId: deferStatus.groupId ?? '',
3179
- });
3180
- }
3181
- if (plaintext === null) {
3182
- client._clientLog.debug(`group.v2.pull decrypt returned null: group=${gid}, seq=${seq}`);
3183
- continue;
3184
- }
3185
- plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
3186
- plaintext.group_id = gid;
3187
- plaintext.group_aid = eventGroupAid;
3188
- plaintext.need_render = typeof msg.need_render === 'boolean' ? msg.need_render : plaintext.need_render !== false;
3189
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
3190
- await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
3191
- }
3192
- else {
3193
- client._markPublishedSeq(ns, seq);
3194
- }
3195
- decrypted.push(plaintext);
3196
- client._logMessageDebug('decrypt-ok', 'group.v2.pull', 'group.message_created', plaintext);
3197
- }
3198
- commitTarget = Math.max(pageContigBefore, effectiveFloor, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, ownsCursor ? cursorCurrentSeq : 0, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
3199
- if (ownsCursor) {
3200
- this.ensureDeliveryCurrent(ns, generation);
3201
- const window = client._seqTracker.getSyncState?.(ns);
3202
- const deliveryTarget = window && commitTarget >= window.tail - 1
3203
- ? Math.max(commitTarget, window.head) : commitTarget;
3204
- if (deliveryTarget > pageContigBefore) {
3205
- await client._drainOrderedMessages(ns, undefined, false, false, 'pull', deliveryTarget);
3206
- }
3207
- this.ensureDeliveryCurrent(ns, generation);
3208
- }
3209
- });
3210
- let ackSeq = pageContigBefore;
3211
- let contigAdvanced = false;
3212
- if (ownsCursor) {
3213
- try {
3214
- this.ensureDeliveryCurrent(ns, generation);
3215
- if (client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) === false)
3216
- throw new Error('pull invalidated');
3217
- client._delivery.rememberCommittedSeqState?.(ns);
3218
- pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
3219
- if (commitTarget > pageContigBefore) {
3220
- trackerMutated = true;
3221
- client._seqTracker.forceContiguousSeq(ns, commitTarget);
3222
- if (effectiveFloor > pageContigBefore) {
3223
- client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
3273
+ const deferStatus = {};
3274
+ let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
3275
+ if (deferStatus.deferred) {
3276
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
3224
3277
  }
3225
- if (deferredServerCursor > pageContigBefore && pageCount === 1) {
3226
- client._clientLog.info(`group.v2.pull restore local A from deferred server ACK cursor: group=${gid}, ns=${ns}, contiguous=${pageContigBefore} -> current_seq=${deferredServerCursor}`);
3278
+ if (deferStatus.deferred && deferStatus.fromAid) {
3279
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
3280
+ deferredKeyFetches.set(key, {
3281
+ fromAid: deferStatus.fromAid,
3282
+ senderDeviceId: deferStatus.senderDeviceId ?? '',
3283
+ groupId: deferStatus.groupId ?? '',
3284
+ });
3227
3285
  }
3228
- if (seqs.length > 0) {
3229
- client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
3286
+ if (plaintext === null) {
3287
+ client._clientLog.debug(`group.v2.pull decrypt returned null: group=${gid}, seq=${seq}`);
3288
+ continue;
3289
+ }
3290
+ plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
3291
+ plaintext.group_id = gid;
3292
+ plaintext.group_aid = eventGroupAid;
3293
+ plaintext.need_render = typeof msg.need_render === 'boolean' ? msg.need_render : plaintext.need_render !== false;
3294
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
3295
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
3230
3296
  }
3297
+ else {
3298
+ client._markPublishedSeq(ns, seq);
3299
+ }
3300
+ collectMessage(plaintext);
3301
+ client._logMessageDebug('decrypt-ok', 'group.v2.pull', 'group.message_created', plaintext);
3231
3302
  }
3232
- ackSeq = client._seqTracker.getContiguousSeq(ns);
3233
- contigAdvanced = ackSeq !== pageContigBefore;
3234
- if (contigAdvanced) {
3235
- await client._commitSeqTrackerState(ns);
3303
+ commitTarget = Math.max(pageContigBefore, effectiveFloor, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, ownsCursor ? cursorCurrentSeq : 0, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
3304
+ if (ownsCursor) {
3305
+ this.ensureDeliveryCurrent(ns, generation);
3306
+ const window = client._seqTracker.getSyncState?.(ns);
3307
+ const deliveryTarget = window && commitTarget >= window.tail - 1
3308
+ ? Math.max(commitTarget, window.head) : commitTarget;
3309
+ if (deliveryTarget > pageContigBefore) {
3310
+ await client._drainOrderedMessages(ns, undefined, false, false, 'pull', deliveryTarget);
3311
+ }
3312
+ this.ensureDeliveryCurrent(ns, generation);
3313
+ }
3314
+ });
3315
+ let ackSeq = pageContigBefore;
3316
+ let contigAdvanced = false;
3317
+ if (ownsCursor) {
3318
+ try {
3236
3319
  this.ensureDeliveryCurrent(ns, generation);
3237
3320
  if (client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) === false)
3238
3321
  throw new Error('pull invalidated');
3239
- if (pageCount === 1 && deferredServerCursor > 0)
3240
- this.clearForwardCursor(ns, ackSeq);
3322
+ client._delivery.rememberCommittedSeqState?.(ns);
3323
+ pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
3324
+ if (commitTarget > pageContigBefore) {
3325
+ trackerMutated = true;
3326
+ client._seqTracker.forceContiguousSeq(ns, commitTarget);
3327
+ if (effectiveFloor > pageContigBefore) {
3328
+ client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
3329
+ }
3330
+ if (deferredServerCursor > pageContigBefore && pageCount === 1) {
3331
+ client._clientLog.info(`group.v2.pull restore local A from deferred server ACK cursor: group=${gid}, ns=${ns}, contiguous=${pageContigBefore} -> current_seq=${deferredServerCursor}`);
3332
+ }
3333
+ if (seqs.length > 0) {
3334
+ client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
3335
+ }
3336
+ }
3337
+ ackSeq = client._seqTracker.getContiguousSeq(ns);
3338
+ contigAdvanced = ackSeq !== pageContigBefore;
3339
+ if (contigAdvanced) {
3340
+ await client._commitSeqTrackerState(ns);
3341
+ this.ensureDeliveryCurrent(ns, generation);
3342
+ if (client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) === false)
3343
+ throw new Error('pull invalidated');
3344
+ if (pageCount === 1 && deferredServerCursor > 0)
3345
+ this.clearForwardCursor(ns, ackSeq);
3346
+ }
3347
+ committedAckSeq = ackSeq;
3241
3348
  }
3242
- committedAckSeq = ackSeq;
3243
- }
3244
- catch (exc) {
3245
- if (trackerMutated && Number(client._v2RuntimeGeneration ?? 0) === generation
3246
- && client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) !== false
3247
- && client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false
3248
- && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
3249
- (client._seqTracker._restoreNamespaceSnapshotPreservingPush ?? client._seqTracker.restoreNamespaceSnapshot)
3250
- .call(client._seqTracker, ns, pageTrackerSnapshot);
3349
+ catch (exc) {
3350
+ if (trackerMutated && Number(client._v2RuntimeGeneration ?? 0) === generation
3351
+ && client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) !== false
3352
+ && client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false
3353
+ && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
3354
+ (client._seqTracker._restoreNamespaceSnapshotPreservingPush ?? client._seqTracker.restoreNamespaceSnapshot)
3355
+ .call(client._seqTracker, ns, pageTrackerSnapshot);
3356
+ }
3357
+ client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
3358
+ throw exc;
3251
3359
  }
3252
- client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
3253
- throw exc;
3254
3360
  }
3361
+ for (const fetch of deferredKeyFetches.values()) {
3362
+ this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
3363
+ }
3364
+ if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
3365
+ this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
3366
+ }
3367
+ // 无本页 cursor.current_seq 时,只有本页成功提交了比已知 cursor
3368
+ // 更高的 A 才新增 ACK 义务;空页不得因已有本地 A 重复确认。
3369
+ const knownServerCursorSeq = hasServerCursor
3370
+ ? cursorCurrentSeq
3371
+ : (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
3372
+ const ackNeeded = ackSeq > 0
3373
+ && ackSeq > lastAutoAckSeq
3374
+ && ownsCursor
3375
+ && ((hasServerCursor && ackSeq > cursorCurrentSeq)
3376
+ || (contigAdvanced && ackSeq > knownServerCursorSeq));
3377
+ if (ackNeeded) {
3378
+ this.recordForwardAck(ns, ackSeq);
3379
+ pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
3380
+ client._clientLog.debug(`group.v2.pull queued piggyback auto-ack: group=${gid}, ns=${ns}, ack_seq=${pendingAckSeq}, raw_count=${messages.length}`);
3381
+ }
3382
+ client._delivery.syncProgress(ns, {
3383
+ rawCount: messages.length,
3384
+ pulledCount: decryptedCount - pageDecryptedBefore,
3385
+ remaining: result.remaining,
3386
+ hasMore: hasMoreField ? hasMore : undefined,
3387
+ currentSeq: client._seqTracker.getContiguousSeq(ns),
3388
+ targetSeq: serverHead,
3389
+ });
3390
+ const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
3391
+ if (!ownsCursor)
3392
+ break;
3393
+ const rawCount = messages.length;
3394
+ if (blockedSeq > 0)
3395
+ break;
3396
+ const shouldContinue = shouldContinueForwardPage({
3397
+ allowEmptyCoverage: true,
3398
+ rawCount,
3399
+ nextAfterSeq,
3400
+ nextAfter,
3401
+ limit,
3402
+ hasMoreField,
3403
+ hasMore,
3404
+ serverHead,
3405
+ observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._delivery.pendingPullUpper?.(ns) ?? 0)),
3406
+ pendingAckSeq,
3407
+ });
3408
+ const shouldContinueForPipeline = shouldContinueForwardPage({
3409
+ allowEmptyCoverage: true,
3410
+ rawCount,
3411
+ nextAfterSeq,
3412
+ nextAfter,
3413
+ limit,
3414
+ hasMoreField,
3415
+ hasMore,
3416
+ serverHead,
3417
+ observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._delivery.pendingPullUpper?.(ns) ?? 0)),
3418
+ pendingAckSeq: 0,
3419
+ });
3420
+ if (!shouldContinue)
3421
+ break;
3422
+ if (shouldContinueForPipeline && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
3423
+ startPipeline(nextAfter);
3424
+ }
3425
+ const tailDelayMs = pipelineRunning ? 0 : v2PullTailDelayMs(rawCount, limit);
3426
+ if (tailDelayMs > 0)
3427
+ await v2Sleep(tailDelayMs);
3428
+ nextAfterSeq = nextAfter;
3255
3429
  }
3256
- for (const fetch of deferredKeyFetches.values()) {
3257
- this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
3258
- }
3259
- if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
3260
- this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
3261
- }
3262
- // 无本页 cursor.current_seq 时,只有本页成功提交了比已知 cursor
3263
- // 更高的 A 才新增 ACK 义务;空页不得因已有本地 A 重复确认。
3264
- const knownServerCursorSeq = hasServerCursor
3265
- ? cursorCurrentSeq
3266
- : (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
3267
- const ackNeeded = ackSeq > 0
3268
- && ackSeq > lastAutoAckSeq
3269
- && ownsCursor
3270
- && ((hasServerCursor && ackSeq > cursorCurrentSeq)
3271
- || (contigAdvanced && ackSeq > knownServerCursorSeq));
3272
- if (ackNeeded) {
3273
- this.recordForwardAck(ns, ackSeq);
3274
- pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
3275
- client._clientLog.debug(`group.v2.pull queued piggyback auto-ack: group=${gid}, ns=${ns}, ack_seq=${pendingAckSeq}, raw_count=${messages.length}`);
3276
- }
3277
- client._delivery.syncProgress(ns, {
3278
- rawCount: messages.length,
3279
- pulledCount: decrypted.length - pageDecryptedBefore,
3280
- remaining: result.remaining,
3281
- hasMore: hasMoreField ? hasMore : undefined,
3282
- currentSeq: client._seqTracker.getContiguousSeq(ns),
3283
- targetSeq: serverHead,
3284
- });
3285
- const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
3286
- if (!ownsCursor)
3287
- break;
3288
- const rawCount = messages.length;
3289
- if (blockedSeq > 0)
3290
- break;
3291
- const shouldContinue = shouldContinueForwardPage({
3292
- allowEmptyCoverage: true,
3293
- rawCount,
3294
- nextAfterSeq,
3295
- nextAfter,
3296
- limit,
3297
- hasMoreField,
3298
- hasMore,
3299
- serverHead,
3300
- observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._delivery.pendingPullUpper?.(ns) ?? 0)),
3301
- pendingAckSeq,
3302
- });
3303
- const shouldContinueForPipeline = shouldContinueForwardPage({
3304
- allowEmptyCoverage: true,
3305
- rawCount,
3306
- nextAfterSeq,
3307
- nextAfter,
3308
- limit,
3309
- hasMoreField,
3310
- hasMore,
3311
- serverHead,
3312
- observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._delivery.pendingPullUpper?.(ns) ?? 0)),
3313
- pendingAckSeq: 0,
3314
- });
3315
- if (!shouldContinue)
3316
- break;
3317
- if (shouldContinueForPipeline && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
3318
- startPipeline(nextAfter);
3430
+ finally {
3431
+ client._syncChannelCoordinator?.releaseResponse(result);
3432
+ syncRetainedPages.delete(result);
3319
3433
  }
3320
- const tailDelayMs = pipelineRunning ? 0 : v2PullTailDelayMs(rawCount, limit);
3321
- if (tailDelayMs > 0)
3322
- await v2Sleep(tailDelayMs);
3323
- nextAfterSeq = nextAfter;
3324
3434
  }
3325
3435
  }
3326
3436
  finally {
3327
3437
  await stopPipeline();
3438
+ for (const response of syncRetainedPages)
3439
+ client._syncChannelCoordinator?.releaseResponse(response);
3440
+ syncRetainedPages.clear();
3328
3441
  }
3329
3442
  if (pageCount >= maxPages) {
3330
3443
  client._clientLog.warn(`group.v2.pull reached max_pages=${maxPages} group=${gid} after_seq=${nextAfterSeq}`);
@@ -3333,11 +3446,15 @@ export class V2E2EECoordinator {
3333
3446
  this.ensureDeliveryCurrent(ns, generation);
3334
3447
  client._clientLog.debug(`group.v2.pull flushing pending auto-ack: group=${gid}, ns=${ns}, ack_seq=${pendingAckSeq}`);
3335
3448
  const ackToFlush = pendingAckSeq;
3336
- await this.confirmForwardGroupAck(gid, ackToFlush, groupAid, background);
3449
+ if (opts?.background === true)
3450
+ client._delivery.scheduleForwardAck(ns, ackToFlush, (seq) => this.confirmForwardGroupAck(gid, seq, groupAid, background));
3451
+ else
3452
+ await this.confirmForwardGroupAck(gid, ackToFlush, groupAid, background);
3337
3453
  if (pendingAckSeq <= ackToFlush)
3338
3454
  pendingAckSeq = 0;
3339
3455
  }
3340
- client._clientLog.debug(`group.v2.pull done: group=${gid}, requested_after_seq=${afterSeq}, pages=${pageCount}, decrypted=${decrypted.length}, ns=${ns}`);
3456
+ client._clientLog.debug(`group.v2.pull done: group=${gid}, requested_after_seq=${afterSeq}, pages=${pageCount}, decrypted=${decryptedCount}, ns=${ns}`);
3457
+ opts?.onMessageCount?.(decryptedCount);
3341
3458
  return decrypted;
3342
3459
  }
3343
3460
  async pullGroupV2(groupId, afterSeq = 0, limit = 50, opts) {