@agentunion/fastaun 0.5.32 → 0.5.34

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 (97) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +28 -26
  3. package/_packed_docs/CHANGELOG.md +58 -0
  4. package/_packed_docs/INDEX.md +17 -3
  5. package/_packed_docs/KITE_DOCS_GUIDE.md +10 -4
  6. package/_packed_docs/protocol/03-Gateway-/350/277/236/346/216/245/346/250/241/345/274/217.md +58 -8
  7. 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 -0
  8. 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 -0
  9. 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
  10. 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
  11. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +16 -13
  12. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +13 -1
  13. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +17 -7
  14. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +55 -14
  15. package/_packed_docs/sdk/09-collab-rpc-manual.md +48 -30
  16. package/_packed_docs/sdk/09-custody-api-manual.md +3 -3
  17. package/_packed_docs/sdk/09-group-rpc-manual.md +53 -6
  18. package/_packed_docs/sdk/09-message-rpc-manual.md +22 -0
  19. package/_packed_docs/sdk/09-meta-rpc-manual.md +2 -1
  20. package/_packed_docs/sdk/09-payload-reference.md +9 -8
  21. package/_packed_docs/sdk/09-proxy-rpc-manual.md +20 -14
  22. package/_packed_docs/sdk/09-storage-rpc-manual.md +1698 -1642
  23. package/_packed_docs/sdk/09-stream-rpc-manual.md +33 -20
  24. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +11 -9
  25. 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 +53 -28
  26. 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 +62 -28
  27. package/_packed_docs/sdk/INDEX.md +21 -9
  28. package/_packed_docs/sdk/README.md +11 -6
  29. package/dist/agent-md.d.ts +2 -1
  30. package/dist/agent-md.js +17 -0
  31. package/dist/agent-md.js.map +1 -1
  32. package/dist/auth.js +8 -2
  33. package/dist/auth.js.map +1 -1
  34. package/dist/client/control-messages.js +4 -4
  35. package/dist/client/control-messages.js.map +1 -1
  36. package/dist/client/delivery.d.ts +5 -0
  37. package/dist/client/delivery.js +146 -32
  38. package/dist/client/delivery.js.map +1 -1
  39. package/dist/client/lifecycle.js +3 -0
  40. package/dist/client/lifecycle.js.map +1 -1
  41. package/dist/client/rpc-budget.d.ts +21 -0
  42. package/dist/client/rpc-budget.js +82 -0
  43. package/dist/client/rpc-budget.js.map +1 -0
  44. package/dist/client/rpc-pipeline.d.ts +3 -0
  45. package/dist/client/rpc-pipeline.js +193 -65
  46. package/dist/client/rpc-pipeline.js.map +1 -1
  47. package/dist/client/sync-channels.d.ts +89 -0
  48. package/dist/client/sync-channels.js +489 -0
  49. package/dist/client/sync-channels.js.map +1 -0
  50. package/dist/client/v2-e2ee.d.ts +4 -1
  51. package/dist/client/v2-e2ee.js +401 -298
  52. package/dist/client/v2-e2ee.js.map +1 -1
  53. package/dist/client.d.ts +9 -4
  54. package/dist/client.js +125 -15
  55. package/dist/client.js.map +1 -1
  56. package/dist/index.d.ts +2 -2
  57. package/dist/index.js.map +1 -1
  58. package/dist/ownership-workflow.d.ts +23 -3
  59. package/dist/ownership-workflow.js +36 -13
  60. package/dist/ownership-workflow.js.map +1 -1
  61. package/dist/pubsub/binding.js +14 -1
  62. package/dist/pubsub/binding.js.map +1 -1
  63. package/dist/pubsub/channel.d.ts +4 -0
  64. package/dist/pubsub/channel.js +26 -0
  65. package/dist/pubsub/channel.js.map +1 -1
  66. package/dist/pubsub/client.d.ts +9 -0
  67. package/dist/pubsub/client.js +154 -31
  68. package/dist/pubsub/client.js.map +1 -1
  69. package/dist/pubsub/credentials.d.ts +3 -0
  70. package/dist/pubsub/credentials.js +57 -20
  71. package/dist/pubsub/credentials.js.map +1 -1
  72. package/dist/pubsub/index.d.ts +3 -0
  73. package/dist/pubsub/index.js +69 -53
  74. package/dist/pubsub/index.js.map +1 -1
  75. package/dist/pubsub/service.d.ts +3 -0
  76. package/dist/pubsub/service.js +167 -12
  77. package/dist/pubsub/service.js.map +1 -1
  78. package/dist/pubsub/types.d.ts +2 -0
  79. package/dist/storage/lowlevel.d.ts +7 -3
  80. package/dist/storage/lowlevel.js +15 -0
  81. package/dist/storage/lowlevel.js.map +1 -1
  82. package/dist/storage/read-cache.js +12 -0
  83. package/dist/storage/read-cache.js.map +1 -1
  84. package/dist/storage/types.d.ts +10 -0
  85. package/dist/storage/types.js.map +1 -1
  86. package/dist/storage/vfs.d.ts +7 -3
  87. package/dist/storage/vfs.js +2 -2
  88. package/dist/storage/vfs.js.map +1 -1
  89. package/dist/tools/cross-sdk-agent.d.ts +62 -6
  90. package/dist/tools/cross-sdk-agent.js +169 -37
  91. package/dist/tools/cross-sdk-agent.js.map +1 -1
  92. package/dist/transport.d.ts +12 -0
  93. package/dist/transport.js +91 -6
  94. package/dist/transport.js.map +1 -1
  95. package/dist/version.d.ts +1 -1
  96. package/dist/version.js +1 -1
  97. package/package.json +1 -1
@@ -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) {
@@ -1316,12 +1316,91 @@ export class V2E2EECoordinator {
1316
1316
  }
1317
1317
  return plaintext;
1318
1318
  }
1319
+ async pullPeerV2Volatile(senderAid, opts = {}) {
1320
+ const client = this.client;
1321
+ const generation = Number(client._v2RuntimeGeneration ?? 0);
1322
+ const inlineGeneration = client._delivery.captureInlineAckGeneration?.();
1323
+ const ns = client._delivery.peerNamespace(senderAid);
1324
+ const limit = Number(opts.limit ?? _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize);
1325
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200)
1326
+ throw new ValidationError('peer limit must be between 1 and 200');
1327
+ const maxPages = Math.max(1, Math.min(100, Number(opts.maxPages ?? 100) || 100));
1328
+ const maxPageBytes = client._syncChannelCoordinator?.volatilePageBytes?.() ?? 262144;
1329
+ await client._ensureV2SessionReady('message.pull');
1330
+ return await client._delivery.runPeerPageSerialized(`${ns}|volatile`, async () => {
1331
+ const messages = [];
1332
+ let pages = 0, rawCount = 0, hasMore = false;
1333
+ while (pages < maxPages) {
1334
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1335
+ if (opts.background && !client._delivery.hasP2pApplicationSubscription())
1336
+ break;
1337
+ let cursor = client._delivery.peerVolatilePullCursor(senderAid);
1338
+ const request = () => client._callRawV2Rpc('message.v2.pull', {
1339
+ cursor_scope: 'peer', sender_aid: senderAid, device_id: client._deviceId, slot_id: client._slotId,
1340
+ storage_scope: 'volatile', limit, max_page_bytes: maxPageBytes,
1341
+ ...(cursor ? { after_volatile_cursor: cursor } : {}),
1342
+ ...(opts.background ? { _rpc_background: true } : { _rpc_foreground: true }),
1343
+ });
1344
+ const reservation = await client._dispatcher?._reserveMessageCapacity?.(limit);
1345
+ try {
1346
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1347
+ let response;
1348
+ try {
1349
+ response = await request();
1350
+ }
1351
+ catch (error) {
1352
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1353
+ if (!cursor || !String(error instanceof Error ? error.message : error).toLowerCase().includes('volatile cursor epoch mismatch'))
1354
+ throw error;
1355
+ client._delivery.setPeerVolatilePullCursor(senderAid, '');
1356
+ cursor = '';
1357
+ response = await request();
1358
+ }
1359
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1360
+ if (!response || response.storage_scope !== 'volatile' || !Array.isArray(response.messages)
1361
+ || response.messages.length > limit || typeof response.volatile_has_more !== 'boolean'
1362
+ || response.has_more !== response.volatile_has_more || typeof response.next_volatile_cursor !== 'string'
1363
+ || new TextEncoder().encode(JSON.stringify(response)).byteLength > maxPageBytes)
1364
+ throw new ValidationError('invalid volatile peer page');
1365
+ const rows = response.messages;
1366
+ for (const row of rows) {
1367
+ if (!isJsonObject(row) || row.storage !== 'volatile' || String(row.sender_aid ?? row.from_aid ?? row.from ?? '') !== senderAid)
1368
+ throw new ValidationError('volatile peer sender mismatch');
1369
+ }
1370
+ const handlers = client._delivery.snapshotApplicationHandlers?.();
1371
+ await this.withApplicationPage(ns, 'pull', async () => {
1372
+ const delivered = await this.publishV2VolatileMessages(rows, 'pull', true);
1373
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1374
+ if (opts.retainMessages !== false)
1375
+ messages.push(...delivered);
1376
+ }, reservation, handlers);
1377
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1378
+ if (!rows.every(row => client._delivery.isVolatileSeen(row)))
1379
+ throw new E2EEError('volatile peer delivery remains pending');
1380
+ client._delivery.setPeerVolatilePullCursor(senderAid, response.next_volatile_cursor);
1381
+ pages++;
1382
+ rawCount += rows.length;
1383
+ hasMore = response.volatile_has_more;
1384
+ if (!hasMore)
1385
+ break;
1386
+ if (response.next_volatile_cursor === cursor)
1387
+ throw new E2EEError('volatile peer cursor made no progress');
1388
+ }
1389
+ finally {
1390
+ reservation?.release();
1391
+ }
1392
+ }
1393
+ return { sender_aid: senderAid, messages, complete: !hasMore, pages, raw_count: rawCount, volatile_has_more: hasMore };
1394
+ });
1395
+ }
1319
1396
  async pullPeerV2(senderAid, opts = {}) {
1320
1397
  const client = this.client;
1321
1398
  const generation = Number(client._v2RuntimeGeneration ?? 0);
1322
1399
  const inlineGeneration = client._delivery.captureInlineAckGeneration?.();
1323
1400
  const pullVersion = opts.peerPullVersion ?? client._delivery.capturePeerTailVersion(senderAid);
1324
1401
  await client._ensureV2SessionReady('message.pull');
1402
+ const volatile = opts.background && opts.window_mode !== 'tail' && client._syncChannelCoordinator?.needsSplit?.()
1403
+ ? await this.pullPeerV2Volatile(senderAid, { background: true, limit: opts.limit, retainMessages: opts.retainMessages }) : null;
1325
1404
  this.ensureDeliveryCurrent(client._delivery.peerNamespace(senderAid), generation, inlineGeneration);
1326
1405
  const ns = await client._delivery.initializePeer(senderAid);
1327
1406
  this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
@@ -1337,14 +1416,14 @@ export class V2E2EECoordinator {
1337
1416
  if (mode !== 'tail' && opts.force !== true && after > client._seqTracker.getContiguousSeq(ns))
1338
1417
  throw new ValidationError('after_seq cannot exceed the peer delivery cursor');
1339
1418
  let volatileCursor = String(opts.after_volatile_cursor ?? client._delivery.peerVolatilePullCursor(senderAid));
1340
- const decoded = [];
1419
+ const decoded = volatile?.messages ?? [];
1341
1420
  let complete = false;
1342
1421
  let head = after;
1343
1422
  let tailVolatileHasMore;
1344
1423
  let tailServerAck = null;
1345
1424
  let pages = 0;
1346
- let rawCount = 0;
1347
- let forwardVolatileHasMore = false;
1425
+ let rawCount = volatile?.raw_count ?? 0;
1426
+ let forwardVolatileHasMore = volatile?.volatile_has_more ?? false;
1348
1427
  let durableAck = 0;
1349
1428
  let initializedAck = false;
1350
1429
  let confirmedAck = 0;
@@ -1380,7 +1459,7 @@ export class V2E2EECoordinator {
1380
1459
  else {
1381
1460
  start = strictWindowSeq(response.scan_after_seq, 'scan_after_seq');
1382
1461
  const retention = safeNonNegativeSeq(response.retention_floor_seq) ?? 0;
1383
- if (start < requestAfter || (!preparing && start > Math.max(requestAfter, retention, opts.force === true ? 0 : committedAck))
1462
+ if (start < Math.max(requestAfter, retention) || (!preparing && start > Math.max(requestAfter, retention, opts.force === true ? 0 : committedAck))
1384
1463
  || end < start || end > pageHead || response.next_after_seq !== end)
1385
1464
  throw new ValidationError('invalid peer Forward proof');
1386
1465
  if (typeof response.persistent_has_more !== 'boolean' || typeof response.volatile_has_more !== 'boolean'
@@ -1492,6 +1571,7 @@ export class V2E2EECoordinator {
1492
1571
  }
1493
1572
  catch (error) {
1494
1573
  fetched.reservation?.release();
1574
+ client._syncChannelCoordinator?.releaseResponse(fetched.response);
1495
1575
  throw error;
1496
1576
  }
1497
1577
  });
@@ -1606,13 +1686,14 @@ export class V2E2EECoordinator {
1606
1686
  }
1607
1687
  finally {
1608
1688
  reservation?.release();
1689
+ client._syncChannelCoordinator?.releaseResponse(response);
1609
1690
  }
1610
1691
  });
1611
1692
  if (!page)
1612
1693
  break;
1613
1694
  this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1614
1695
  rawCount += page.response.messages.length;
1615
- forwardVolatileHasMore = page.response.volatile_has_more === true;
1696
+ forwardVolatileHasMore = volatile ? volatile.volatile_has_more === true : page.response.volatile_has_more === true;
1616
1697
  if (opts.retainMessages !== false)
1617
1698
  decoded.push(...page.messages);
1618
1699
  head = page.head;
@@ -1624,7 +1705,7 @@ export class V2E2EECoordinator {
1624
1705
  }
1625
1706
  volatileCursor = client._delivery.peerVolatilePullCursor(senderAid);
1626
1707
  const more = page.response.persistent_has_more === true || page.response.volatile_has_more === true || page.response.has_more === true;
1627
- complete = !more;
1708
+ complete = !more && !forwardVolatileHasMore;
1628
1709
  client._delivery.syncProgress(ns, { rawCount: page.response.messages.length, pulledCount: page.messages.length, hasMore: more,
1629
1710
  currentSeq: client._seqTracker.getContiguousSeq(ns), targetSeq: head });
1630
1711
  after = page.end;
@@ -1640,7 +1721,9 @@ export class V2E2EECoordinator {
1640
1721
  stopping.abort();
1641
1722
  for (const pending of prefetched) {
1642
1723
  try {
1643
- (await (pending.prepared ?? pending.response))?.reservation?.release();
1724
+ const fetched = await (pending.prepared ?? pending.response);
1725
+ fetched?.reservation?.release();
1726
+ client._syncChannelCoordinator?.releaseResponse(fetched?.response);
1644
1727
  }
1645
1728
  catch { }
1646
1729
  }
@@ -1681,7 +1764,7 @@ export class V2E2EECoordinator {
1681
1764
  || (response.slot_id !== undefined && response.slot_id !== client._slotId))
1682
1765
  throw new ValidationError('peer response scope mismatch');
1683
1766
  }
1684
- async ackPeerV2(senderAid, upToSeq, volatileCursors = [], volatileIds = []) {
1767
+ async ackPeerV2(senderAid, upToSeq, volatileCursors = [], volatileIds = [], background = true) {
1685
1768
  const client = this.client;
1686
1769
  const generation = Number(client._v2RuntimeGeneration ?? 0);
1687
1770
  const inlineGeneration = client._delivery.captureInlineAckGeneration?.();
@@ -1696,7 +1779,7 @@ export class V2E2EECoordinator {
1696
1779
  const result = await client._callRawV2Rpc('message.v2.ack', { cursor_scope: 'peer', sender_aid: senderAid,
1697
1780
  device_id: client._deviceId, slot_id: client._slotId,
1698
1781
  ...(seq ? { up_to_seq: seq } : {}), ...(volatileCursors.length ? { volatile_cursors: volatileCursors } : {}),
1699
- ...(volatileIds.length ? { volatile_ids: volatileIds } : {}), _rpc_background: true });
1782
+ ...(volatileIds.length ? { volatile_ids: volatileIds } : {}), _rpc_background: background, _rpc_foreground: !background });
1700
1783
  this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1701
1784
  this.validatePeerResponse(result, senderAid);
1702
1785
  const actual = safeNonNegativeSeq(result.peer_ack_seq ?? result.effective_ack_seq ?? result.up_to_seq);
@@ -2710,7 +2793,7 @@ export class V2E2EECoordinator {
2710
2793
  throw new ValidationError('group_id is required');
2711
2794
  let groupAid = String(params.group_aid ?? '').trim();
2712
2795
  const afterSeq = strictWindowSeq(params.after_seq ?? 0, 'after_seq');
2713
- const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
2796
+ const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT, 200);
2714
2797
  const ns = `group:${groupId}`;
2715
2798
  const namespaceGeneration = client._delivery.captureSeqNamespaceGeneration?.(ns);
2716
2799
  const explicitCursorParams = isJsonObject(params._group_cursor_params)
@@ -2823,6 +2906,7 @@ export class V2E2EECoordinator {
2823
2906
  await this.pullGroupV2(params.group_id, params.after_seq, params.limit, {
2824
2907
  gateLocked: true,
2825
2908
  explicitAfterSeq: true,
2909
+ retainMessages: false,
2826
2910
  force: params.force === true,
2827
2911
  });
2828
2912
  }
@@ -2871,6 +2955,12 @@ export class V2E2EECoordinator {
2871
2955
  }), opts?.background !== false);
2872
2956
  }
2873
2957
  const decrypted = [];
2958
+ let decryptedCount = 0;
2959
+ const collectMessage = (message) => {
2960
+ decryptedCount += 1;
2961
+ if (opts?.retainMessages !== false)
2962
+ decrypted.push(message);
2963
+ };
2874
2964
  const wireGroupId = String(opts?.wireGroupId ?? groupId ?? '').trim() || gid;
2875
2965
  const cursorParams = opts?.cursorParams ?? {};
2876
2966
  const ownsCursor = opts?.ownsCursor !== false;
@@ -2886,18 +2976,21 @@ export class V2E2EECoordinator {
2886
2976
  let lastAutoAckSeq = 0;
2887
2977
  let pendingAckSeq = deferredForwardAck;
2888
2978
  let committedAckSeq = ownsCursor ? client._seqTracker.getContiguousSeq(ns) : 0;
2979
+ const syncRetainedPages = new Set();
2889
2980
  const fetchPage = async (after, ack) => {
2890
2981
  const key = pullGateKeyForClient(client, 'group.v2.pull', {
2891
2982
  group_id: gid, after_seq: after, force: opts?.force === true, limit, _group_cursor_params: cursorParams,
2892
2983
  }, `${ns}|forward|after=${after}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
2893
2984
  return await client._runPullSerialized(key, async () => {
2894
2985
  this.ensureDeliveryCurrent(ns, generation);
2895
- return await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2986
+ const response = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2896
2987
  group_id: wireGroupId, after_seq: after, limit, ...cursorParams,
2897
2988
  ...(opts?.force ? { force: true } : {}),
2898
2989
  ...(ack > 0 ? { ack_up_to_seq: Math.min(ack, committedAckSeq) } : {}),
2899
2990
  _rpc_background: background, _rpc_foreground: !background,
2900
2991
  }, groupAid), key);
2992
+ syncRetainedPages.add(response);
2993
+ return response;
2901
2994
  }, background);
2902
2995
  };
2903
2996
  const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
@@ -2998,92 +3091,121 @@ export class V2E2EECoordinator {
2998
3091
  else {
2999
3092
  result = await fetchPage(nextAfterSeq, ackUpToSeq);
3000
3093
  }
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);
3094
+ try {
3095
+ if (ownsCursor)
3096
+ this.ensureDeliveryCurrent(ns, generation);
3097
+ if (ackUpToSeq > 0) {
3098
+ const actualAckSeq = client._delivery.resolveGroupPullAckSeq(result, ackUpToSeq);
3099
+ if (actualAckSeq >= ackUpToSeq) {
3100
+ if (inlineAckGeneration !== null) {
3101
+ client._delivery.onGroupV2PullAckResult(gid, result, ackUpToSeq, inlineAckGeneration);
3102
+ }
3103
+ if (pendingAckSeq <= ackUpToSeq)
3104
+ pendingAckSeq = 0;
3105
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
3106
+ this.clearForwardAck(ns, ackUpToSeq);
3107
+ }
3108
+ else {
3109
+ client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
3008
3110
  }
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
3111
  }
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 () => {
3112
+ const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
3113
+ if (resultGroupAid)
3114
+ groupAid = resultGroupAid;
3115
+ const eventGroupAid = resultGroupAid || groupAid || gid;
3116
+ const pageMessages = (Array.isArray(result.messages) ? result.messages : []);
3117
+ const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
3118
+ const cursor = isJsonObject(result.cursor) ? result.cursor : null;
3119
+ const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
3120
+ const hasMore = result.has_more === true;
3121
+ const serverHead = Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0);
3122
+ 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
3123
  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}`);
3124
+ client._logMessageDebug('pull-raw', 'group.v2.pull', 'group.message_created', msg);
3125
+ }
3126
+ const seqs = messages
3127
+ .map((msg) => Number(msg.seq ?? 0))
3128
+ .filter((seq) => Number.isFinite(seq) && seq > 0);
3129
+ const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
3130
+ const namespaceGeneration = client._delivery.captureSeqNamespaceGeneration?.(ns);
3131
+ let pageTrackerSnapshot = null;
3132
+ let trackerMutated = false;
3133
+ let pageMaxSeq = nextAfterSeq;
3134
+ if (seqs.length > 0) {
3135
+ pageMaxSeq = Math.max(...seqs);
3136
+ }
3137
+ const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
3138
+ if (!pipelineRunning && ownsCursor && shouldContinueForwardPage({
3139
+ allowEmptyCoverage: true,
3140
+ rawCount: messages.length, nextAfterSeq, nextAfter: pageMaxSeq, limit,
3141
+ hasMoreField, hasMore, serverHead,
3142
+ observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._delivery.pendingPullUpper?.(ns) ?? 0)),
3143
+ pendingAckSeq: 0,
3144
+ }))
3145
+ startPipeline(pageMaxSeq);
3146
+ const deferredKeyFetches = new Map();
3147
+ let blockedSeq = 0;
3148
+ const pageDecryptedBefore = decryptedCount;
3149
+ const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
3150
+ const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
3151
+ const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
3152
+ const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq'));
3153
+ const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
3154
+ const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
3155
+ let commitTarget = pageContigBefore;
3156
+ await this.withApplicationPage(ns, 'pull', async () => {
3157
+ for (const msg of messages) {
3158
+ const seq = Number(msg.seq ?? 0);
3159
+ if (!Number.isFinite(seq) || seq <= 0)
3083
3160
  continue;
3084
- }
3085
- if (payloadObj) {
3086
- if (!['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
3161
+ const version = String(msg.version ?? 'v2');
3162
+ if (version === 'v1') {
3163
+ const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
3164
+ const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
3165
+ const payloadObj = isJsonObject(payload) ? payload : null;
3166
+ const payloadType = payloadObj ? String(payloadObj.type ?? '').trim() : '';
3167
+ // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
3168
+ if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
3169
+ && client._delivery.recallEventFromGroupMessage(msg)) {
3170
+ await client._delivery.publishGroupRecallTombstone(gid, seq, {
3171
+ ...msg,
3172
+ group_id: gid,
3173
+ group_aid: eventGroupAid,
3174
+ }, 'pull');
3175
+ client._markPublishedSeq(ns, seq);
3176
+ client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
3177
+ continue;
3178
+ }
3179
+ if (payloadObj) {
3180
+ if (!['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
3181
+ let v1Msg = {
3182
+ message_id: String(msg.message_id ?? ''),
3183
+ from: String(msg.from_aid ?? ''),
3184
+ group_id: gid,
3185
+ group_aid: eventGroupAid,
3186
+ seq: msg.seq,
3187
+ type: String(msg.type ?? ''),
3188
+ timestamp: msg.t_server,
3189
+ payload,
3190
+ encrypted: false,
3191
+ need_render: msg.need_render !== false,
3192
+ };
3193
+ if (msg.sender_device_id !== undefined)
3194
+ v1Msg.sender_device_id = msg.sender_device_id;
3195
+ v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
3196
+ attachGatewayProximity(v1Msg, msg);
3197
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
3198
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
3199
+ }
3200
+ else {
3201
+ client._markPublishedSeq(ns, seq);
3202
+ }
3203
+ collectMessage(v1Msg);
3204
+ client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
3205
+ continue;
3206
+ }
3207
+ }
3208
+ else if (payload !== undefined && payload !== null) {
3087
3209
  let v1Msg = {
3088
3210
  message_id: String(msg.message_id ?? ''),
3089
3211
  from: String(msg.from_aid ?? ''),
@@ -3100,231 +3222,211 @@ export class V2E2EECoordinator {
3100
3222
  v1Msg.sender_device_id = msg.sender_device_id;
3101
3223
  v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
3102
3224
  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);
3225
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
3226
+ collectMessage(v1Msg);
3110
3227
  client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
3111
3228
  continue;
3112
3229
  }
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);
3230
+ if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
3231
+ const deferStatus = {};
3232
+ const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
3233
+ if (deferStatus.deferred) {
3234
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
3235
+ if (deferStatus.fromAid) {
3236
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
3237
+ deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
3238
+ }
3152
3239
  }
3153
- else {
3154
- client._markPublishedSeq(ns, seq);
3240
+ if (plaintext) {
3241
+ plaintext.group_id = gid;
3242
+ plaintext.group_aid = eventGroupAid;
3243
+ plaintext.need_render = typeof msg.need_render === 'boolean' ? msg.need_render : plaintext.need_render !== false;
3244
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
3245
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
3246
+ }
3247
+ else {
3248
+ client._markPublishedSeq(ns, seq);
3249
+ }
3250
+ collectMessage(plaintext);
3155
3251
  }
3156
- decrypted.push(plaintext);
3157
3252
  }
3253
+ else {
3254
+ client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
3255
+ }
3256
+ continue;
3158
3257
  }
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)`);
3258
+ if (version !== 'v2') {
3259
+ client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
3260
+ continue;
3161
3261
  }
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}`);
3262
+ const deferStatus = {};
3263
+ let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
3264
+ if (deferStatus.deferred) {
3265
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
3224
3266
  }
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}`);
3267
+ if (deferStatus.deferred && deferStatus.fromAid) {
3268
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
3269
+ deferredKeyFetches.set(key, {
3270
+ fromAid: deferStatus.fromAid,
3271
+ senderDeviceId: deferStatus.senderDeviceId ?? '',
3272
+ groupId: deferStatus.groupId ?? '',
3273
+ });
3227
3274
  }
3228
- if (seqs.length > 0) {
3229
- client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
3275
+ if (plaintext === null) {
3276
+ client._clientLog.debug(`group.v2.pull decrypt returned null: group=${gid}, seq=${seq}`);
3277
+ continue;
3278
+ }
3279
+ plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
3280
+ plaintext.group_id = gid;
3281
+ plaintext.group_aid = eventGroupAid;
3282
+ plaintext.need_render = typeof msg.need_render === 'boolean' ? msg.need_render : plaintext.need_render !== false;
3283
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
3284
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
3285
+ }
3286
+ else {
3287
+ client._markPublishedSeq(ns, seq);
3230
3288
  }
3289
+ collectMessage(plaintext);
3290
+ client._logMessageDebug('decrypt-ok', 'group.v2.pull', 'group.message_created', plaintext);
3231
3291
  }
3232
- ackSeq = client._seqTracker.getContiguousSeq(ns);
3233
- contigAdvanced = ackSeq !== pageContigBefore;
3234
- if (contigAdvanced) {
3235
- await client._commitSeqTrackerState(ns);
3292
+ commitTarget = Math.max(pageContigBefore, effectiveFloor, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, ownsCursor ? cursorCurrentSeq : 0, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
3293
+ if (ownsCursor) {
3294
+ this.ensureDeliveryCurrent(ns, generation);
3295
+ const window = client._seqTracker.getSyncState?.(ns);
3296
+ const deliveryTarget = window && commitTarget >= window.tail - 1
3297
+ ? Math.max(commitTarget, window.head) : commitTarget;
3298
+ if (deliveryTarget > pageContigBefore) {
3299
+ await client._drainOrderedMessages(ns, undefined, false, false, 'pull', deliveryTarget);
3300
+ }
3301
+ this.ensureDeliveryCurrent(ns, generation);
3302
+ }
3303
+ });
3304
+ let ackSeq = pageContigBefore;
3305
+ let contigAdvanced = false;
3306
+ if (ownsCursor) {
3307
+ try {
3236
3308
  this.ensureDeliveryCurrent(ns, generation);
3237
3309
  if (client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) === false)
3238
3310
  throw new Error('pull invalidated');
3239
- if (pageCount === 1 && deferredServerCursor > 0)
3240
- this.clearForwardCursor(ns, ackSeq);
3311
+ client._delivery.rememberCommittedSeqState?.(ns);
3312
+ pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
3313
+ if (commitTarget > pageContigBefore) {
3314
+ trackerMutated = true;
3315
+ client._seqTracker.forceContiguousSeq(ns, commitTarget);
3316
+ if (effectiveFloor > pageContigBefore) {
3317
+ client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
3318
+ }
3319
+ if (deferredServerCursor > pageContigBefore && pageCount === 1) {
3320
+ client._clientLog.info(`group.v2.pull restore local A from deferred server ACK cursor: group=${gid}, ns=${ns}, contiguous=${pageContigBefore} -> current_seq=${deferredServerCursor}`);
3321
+ }
3322
+ if (seqs.length > 0) {
3323
+ client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
3324
+ }
3325
+ }
3326
+ ackSeq = client._seqTracker.getContiguousSeq(ns);
3327
+ contigAdvanced = ackSeq !== pageContigBefore;
3328
+ if (contigAdvanced) {
3329
+ await client._commitSeqTrackerState(ns);
3330
+ this.ensureDeliveryCurrent(ns, generation);
3331
+ if (client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) === false)
3332
+ throw new Error('pull invalidated');
3333
+ if (pageCount === 1 && deferredServerCursor > 0)
3334
+ this.clearForwardCursor(ns, ackSeq);
3335
+ }
3336
+ committedAckSeq = ackSeq;
3241
3337
  }
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);
3338
+ catch (exc) {
3339
+ if (trackerMutated && Number(client._v2RuntimeGeneration ?? 0) === generation
3340
+ && client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) !== false
3341
+ && client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false
3342
+ && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
3343
+ (client._seqTracker._restoreNamespaceSnapshotPreservingPush ?? client._seqTracker.restoreNamespaceSnapshot)
3344
+ .call(client._seqTracker, ns, pageTrackerSnapshot);
3345
+ }
3346
+ client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
3347
+ throw exc;
3251
3348
  }
3252
- client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
3253
- throw exc;
3254
3349
  }
3350
+ for (const fetch of deferredKeyFetches.values()) {
3351
+ this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
3352
+ }
3353
+ if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
3354
+ this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
3355
+ }
3356
+ // 无本页 cursor.current_seq 时,只有本页成功提交了比已知 cursor
3357
+ // 更高的 A 才新增 ACK 义务;空页不得因已有本地 A 重复确认。
3358
+ const knownServerCursorSeq = hasServerCursor
3359
+ ? cursorCurrentSeq
3360
+ : (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
3361
+ const ackNeeded = ackSeq > 0
3362
+ && ackSeq > lastAutoAckSeq
3363
+ && ownsCursor
3364
+ && ((hasServerCursor && ackSeq > cursorCurrentSeq)
3365
+ || (contigAdvanced && ackSeq > knownServerCursorSeq));
3366
+ if (ackNeeded) {
3367
+ this.recordForwardAck(ns, ackSeq);
3368
+ pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
3369
+ client._clientLog.debug(`group.v2.pull queued piggyback auto-ack: group=${gid}, ns=${ns}, ack_seq=${pendingAckSeq}, raw_count=${messages.length}`);
3370
+ }
3371
+ client._delivery.syncProgress(ns, {
3372
+ rawCount: messages.length,
3373
+ pulledCount: decryptedCount - pageDecryptedBefore,
3374
+ remaining: result.remaining,
3375
+ hasMore: hasMoreField ? hasMore : undefined,
3376
+ currentSeq: client._seqTracker.getContiguousSeq(ns),
3377
+ targetSeq: serverHead,
3378
+ });
3379
+ const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
3380
+ if (!ownsCursor)
3381
+ break;
3382
+ const rawCount = messages.length;
3383
+ if (blockedSeq > 0)
3384
+ break;
3385
+ const shouldContinue = shouldContinueForwardPage({
3386
+ allowEmptyCoverage: true,
3387
+ rawCount,
3388
+ nextAfterSeq,
3389
+ nextAfter,
3390
+ limit,
3391
+ hasMoreField,
3392
+ hasMore,
3393
+ serverHead,
3394
+ observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._delivery.pendingPullUpper?.(ns) ?? 0)),
3395
+ pendingAckSeq,
3396
+ });
3397
+ const shouldContinueForPipeline = shouldContinueForwardPage({
3398
+ allowEmptyCoverage: true,
3399
+ rawCount,
3400
+ nextAfterSeq,
3401
+ nextAfter,
3402
+ limit,
3403
+ hasMoreField,
3404
+ hasMore,
3405
+ serverHead,
3406
+ observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._delivery.pendingPullUpper?.(ns) ?? 0)),
3407
+ pendingAckSeq: 0,
3408
+ });
3409
+ if (!shouldContinue)
3410
+ break;
3411
+ if (shouldContinueForPipeline && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
3412
+ startPipeline(nextAfter);
3413
+ }
3414
+ const tailDelayMs = pipelineRunning ? 0 : v2PullTailDelayMs(rawCount, limit);
3415
+ if (tailDelayMs > 0)
3416
+ await v2Sleep(tailDelayMs);
3417
+ nextAfterSeq = nextAfter;
3255
3418
  }
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);
3419
+ finally {
3420
+ client._syncChannelCoordinator?.releaseResponse(result);
3421
+ syncRetainedPages.delete(result);
3319
3422
  }
3320
- const tailDelayMs = pipelineRunning ? 0 : v2PullTailDelayMs(rawCount, limit);
3321
- if (tailDelayMs > 0)
3322
- await v2Sleep(tailDelayMs);
3323
- nextAfterSeq = nextAfter;
3324
3423
  }
3325
3424
  }
3326
3425
  finally {
3327
3426
  await stopPipeline();
3427
+ for (const response of syncRetainedPages)
3428
+ client._syncChannelCoordinator?.releaseResponse(response);
3429
+ syncRetainedPages.clear();
3328
3430
  }
3329
3431
  if (pageCount >= maxPages) {
3330
3432
  client._clientLog.warn(`group.v2.pull reached max_pages=${maxPages} group=${gid} after_seq=${nextAfterSeq}`);
@@ -3337,7 +3439,8 @@ export class V2E2EECoordinator {
3337
3439
  if (pendingAckSeq <= ackToFlush)
3338
3440
  pendingAckSeq = 0;
3339
3441
  }
3340
- client._clientLog.debug(`group.v2.pull done: group=${gid}, requested_after_seq=${afterSeq}, pages=${pageCount}, decrypted=${decrypted.length}, ns=${ns}`);
3442
+ client._clientLog.debug(`group.v2.pull done: group=${gid}, requested_after_seq=${afterSeq}, pages=${pageCount}, decrypted=${decryptedCount}, ns=${ns}`);
3443
+ opts?.onMessageCount?.(decryptedCount);
3341
3444
  return decrypted;
3342
3445
  }
3343
3446
  async pullGroupV2(groupId, afterSeq = 0, limit = 50, opts) {