@agentunion/fastaun 0.5.16 → 0.5.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +336 -260
  2. package/_packed_docs/CHANGELOG.md +336 -260
  3. package/_packed_docs/INDEX.md +36 -5
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +13 -5
  5. package/_packed_docs/agent.md/SCHEMA.md +52 -33
  6. package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +1170 -1164
  7. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +1215 -1213
  8. package/_packed_docs/protocol/01-/350/272/253/344/273/275/344/270/216/345/207/255/350/257/201/345/215/217/350/256/256-auth.md +95 -6
  9. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +58 -43
  10. package/_packed_docs/protocol/07-/351/224/231/350/257/257/347/240/201/344/270/216/347/212/266/346/200/201/346/234/272.md +19 -4
  11. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +80 -17
  12. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +25 -2
  13. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +69 -10
  14. package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +7 -4
  15. package/_packed_docs/sdk/09-group-rpc-manual.md +167 -35
  16. package/_packed_docs/sdk/09-message-rpc-manual.md +72 -42
  17. package/_packed_docs/sdk/09-storage-rpc-manual.md +48 -18
  18. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +16 -12
  19. package/_packed_docs/sdk/INDEX.md +14 -13
  20. package/dist/agent-md-schema.js +2 -0
  21. package/dist/agent-md-schema.js.map +1 -1
  22. package/dist/agent-md.d.ts +3 -1
  23. package/dist/agent-md.js +10 -2
  24. package/dist/agent-md.js.map +1 -1
  25. package/dist/aid-store.d.ts +58 -0
  26. package/dist/aid-store.js +234 -3
  27. package/dist/aid-store.js.map +1 -1
  28. package/dist/aid.d.ts +17 -1
  29. package/dist/aid.js +48 -2
  30. package/dist/aid.js.map +1 -1
  31. package/dist/client/delivery.d.ts +19 -0
  32. package/dist/client/delivery.js +264 -31
  33. package/dist/client/delivery.js.map +1 -1
  34. package/dist/client/group-state.d.ts +4 -0
  35. package/dist/client/group-state.js +63 -0
  36. package/dist/client/group-state.js.map +1 -1
  37. package/dist/client/rpc-pipeline.js +29 -14
  38. package/dist/client/rpc-pipeline.js.map +1 -1
  39. package/dist/client/v2-e2ee.d.ts +17 -2
  40. package/dist/client/v2-e2ee.js +971 -520
  41. package/dist/client/v2-e2ee.js.map +1 -1
  42. package/dist/client.d.ts +3 -0
  43. package/dist/client.js +120 -19
  44. package/dist/client.js.map +1 -1
  45. package/dist/facades.d.ts +7 -0
  46. package/dist/facades.js +11 -0
  47. package/dist/facades.js.map +1 -1
  48. package/dist/group-fs.js +7 -5
  49. package/dist/group-fs.js.map +1 -1
  50. package/dist/index.d.ts +3 -2
  51. package/dist/index.js +1 -0
  52. package/dist/index.js.map +1 -1
  53. package/dist/ownership-proof.d.ts +40 -0
  54. package/dist/ownership-proof.js +140 -0
  55. package/dist/ownership-proof.js.map +1 -0
  56. package/dist/storage/vfs.d.ts +1 -0
  57. package/dist/storage/vfs.js +61 -6
  58. package/dist/storage/vfs.js.map +1 -1
  59. package/dist/tools/cross-sdk-agent.js +31 -1
  60. package/dist/tools/cross-sdk-agent.js.map +1 -1
  61. package/dist/types.d.ts +16 -1
  62. package/dist/version.d.ts +1 -1
  63. package/dist/version.js +1 -1
  64. package/package.json +14 -14
@@ -10,6 +10,52 @@ const V2_GROUP_STALE_BOOTSTRAP_CODE = -33054;
10
10
  const V2_PULL_DEFAULT_PAGE_LIMIT = 50;
11
11
  const V2_PULL_MAX_PAGE_LIMIT = 50;
12
12
  const V2_SENDER_IK_SYNC_FETCH_TIMEOUT_MS = 3_000;
13
+ const V2_PULL_PIPELINE_QUEUE_CAPACITY = 100;
14
+ class V2PullPageQueue {
15
+ capacity;
16
+ items = [];
17
+ takers = [];
18
+ putters = [];
19
+ queuedWeight = 0;
20
+ closed = false;
21
+ constructor(capacity) {
22
+ this.capacity = capacity;
23
+ }
24
+ async put(item, weight = 1) {
25
+ const itemWeight = Math.max(1, Math.trunc(weight));
26
+ while (!this.closed && this.items.length > 0 && this.queuedWeight + itemWeight > this.capacity) {
27
+ await new Promise((resolve) => this.putters.push(resolve));
28
+ }
29
+ if (this.closed)
30
+ return false;
31
+ const taker = this.takers.shift();
32
+ if (taker)
33
+ taker(item);
34
+ else {
35
+ this.items.push(item);
36
+ this.queuedWeight += itemWeight;
37
+ }
38
+ return true;
39
+ }
40
+ async take() {
41
+ const item = this.items.shift();
42
+ if (item !== undefined) {
43
+ this.queuedWeight = Math.max(0, this.queuedWeight - Math.max(1, Math.trunc(Number(item.weight ?? 1))));
44
+ this.putters.shift()?.();
45
+ return item;
46
+ }
47
+ if (this.closed)
48
+ return undefined;
49
+ return await new Promise((resolve) => this.takers.push(resolve));
50
+ }
51
+ close() {
52
+ this.closed = true;
53
+ for (const resolve of this.takers.splice(0))
54
+ resolve(undefined);
55
+ for (const resolve of this.putters.splice(0))
56
+ resolve();
57
+ }
58
+ }
13
59
  function pullGateKeyForClient(client, method, params, fallback) {
14
60
  const key = client._rpcPipeline?.pullGateKeyForCall?.(method, params);
15
61
  return typeof key === 'string' && key ? key : fallback;
@@ -773,6 +819,12 @@ export class V2E2EECoordinator {
773
819
  }
774
820
  continue;
775
821
  }
822
+ if (!entry.groupId && String(entry.msg.storage ?? '') === 'volatile') {
823
+ await this.publishPendingV2VolatileMessage(entry.msg, plaintext, 'pending_retry');
824
+ pending.delete(key);
825
+ client._clientLog.debug(`V2 sender IK pending retry delivered: key=${key}`);
826
+ continue;
827
+ }
776
828
  pending.delete(key);
777
829
  const seq = Number(entry.msg.seq ?? 0);
778
830
  if (entry.groupId) {
@@ -795,6 +847,30 @@ export class V2E2EECoordinator {
795
847
  }
796
848
  }
797
849
  }
850
+ async publishPendingV2VolatileMessage(rawMessage, plaintext, source) {
851
+ const delivery = this.client._delivery;
852
+ const claim = delivery.beginVolatileDelivery(rawMessage);
853
+ if (claim === 'inflight')
854
+ return;
855
+ if (claim === 'seen') {
856
+ await delivery.ackVolatileMessages([rawMessage], true, false);
857
+ return;
858
+ }
859
+ try {
860
+ const publicMessage = delivery.publicVolatileMessage(plaintext);
861
+ for (const key of ['storage', 'volatile_cursor', 'created_at', 'expires_at', 'persist']) {
862
+ if (Object.prototype.hasOwnProperty.call(rawMessage, key))
863
+ publicMessage[key] = rawMessage[key];
864
+ }
865
+ const appEvent = delivery.p2pAppEventForMessage(publicMessage);
866
+ await this.client._publishAppEvent(appEvent.event, appEvent.payload, source);
867
+ delivery.markVolatileSeen(rawMessage);
868
+ }
869
+ finally {
870
+ delivery.finishVolatileDelivery(rawMessage);
871
+ }
872
+ await delivery.ackVolatileMessages([rawMessage], true, false);
873
+ }
798
874
  async buildV2P2PEnvelope(opts) {
799
875
  const client = this.client;
800
876
  if (!client._v2Session) {
@@ -841,6 +917,10 @@ export class V2E2EECoordinator {
841
917
  if (peerDevices.length === 0) {
842
918
  throw new E2EEError(`V2 bootstrap: no devices found for ${to}`);
843
919
  }
920
+ const targetDeviceId = String(opts.deviceId ?? '').trim();
921
+ if (targetDeviceId && !peerDevices.some((dev) => String(dev.device_id ?? '').trim() === targetDeviceId)) {
922
+ throw new E2EEError(`V2 bootstrap: target device ${targetDeviceId} not found for ${to}`);
923
+ }
844
924
  let targetSet = useCache ? this.getTargetSetCache(to) : undefined;
845
925
  if (targetSet) {
846
926
  targetSet = this.cloneP2PTargetSet(targetSet);
@@ -926,6 +1006,20 @@ export class V2E2EECoordinator {
926
1006
  if (targetSet.targets.length === 0) {
927
1007
  throw new E2EEError(`V2 bootstrap: no usable devices found for ${to}`);
928
1008
  }
1009
+ if (targetDeviceId) {
1010
+ let peerTargetFound = false;
1011
+ targetSet.targets = targetSet.targets.filter((target) => {
1012
+ const isPeerTarget = target.aid === to
1013
+ && (target.deviceId === targetDeviceId || (wrapPolicy.scope === 'aid' && !target.deviceId))
1014
+ && target.role !== 'self_sync';
1015
+ if (isPeerTarget)
1016
+ peerTargetFound = true;
1017
+ return isPeerTarget || target.role === 'self_sync';
1018
+ });
1019
+ if (!peerTargetFound) {
1020
+ throw new E2EEError(`V2 bootstrap: target device ${targetDeviceId} not found for ${to}`);
1021
+ }
1022
+ }
929
1023
  const envelope = encryptP2PMessage(session.getSenderIdentity(), targetSet, opts.payload, {
930
1024
  messageId: opts.messageId,
931
1025
  timestamp: opts.timestamp,
@@ -954,6 +1048,7 @@ export class V2E2EECoordinator {
954
1048
  const client = this.client;
955
1049
  await client._ensureV2SessionReady('message.send', 'V2 session not initialized; encrypted message.send requires V2 (V1 E2EE removed)');
956
1050
  const toAid = String(to ?? '').trim();
1051
+ const targetDeviceId = String(opts?.deviceId ?? '').trim();
957
1052
  if (!toAid)
958
1053
  throw new ValidationError("message.send requires 'to'");
959
1054
  if (!isJsonObject(payload))
@@ -971,6 +1066,7 @@ export class V2E2EECoordinator {
971
1066
  payload,
972
1067
  messageId,
973
1068
  timestamp: opts?.timestamp,
1069
+ deviceId: targetDeviceId || undefined,
974
1070
  protectedHeaders: opts?.protectedHeaders,
975
1071
  context: opts?.context,
976
1072
  useCache,
@@ -979,6 +1075,8 @@ export class V2E2EECoordinator {
979
1075
  to: toAid,
980
1076
  payload: envelope,
981
1077
  message_id: messageId,
1078
+ ...(targetDeviceId ? { device_id: targetDeviceId } : {}),
1079
+ ...(opts?.ttl !== undefined ? { ttl: opts.ttl } : {}),
982
1080
  encrypt: false,
983
1081
  _skip_send_result_envelope: true,
984
1082
  });
@@ -992,6 +1090,7 @@ export class V2E2EECoordinator {
992
1090
  payload,
993
1091
  message_id: messageId,
994
1092
  timestamp: opts?.timestamp,
1093
+ ttl: opts?.ttl,
995
1094
  protected_headers: opts?.protectedHeaders,
996
1095
  context: opts?.context,
997
1096
  }, result, true);
@@ -1007,6 +1106,7 @@ export class V2E2EECoordinator {
1007
1106
  payload,
1008
1107
  message_id: messageId,
1009
1108
  timestamp: opts?.timestamp,
1109
+ ttl: opts?.ttl,
1010
1110
  protected_headers: opts?.protectedHeaders,
1011
1111
  context: opts?.context,
1012
1112
  }, result, true);
@@ -1043,6 +1143,11 @@ export class V2E2EECoordinator {
1043
1143
  payload: payload,
1044
1144
  encrypted: false,
1045
1145
  };
1146
+ if (msg.sender_device_id !== undefined)
1147
+ plaintext.sender_device_id = msg.sender_device_id;
1148
+ const direction = String(msg.direction ?? '').trim();
1149
+ if (direction)
1150
+ plaintext.direction = direction;
1046
1151
  attachGatewayProximity(plaintext, msg);
1047
1152
  }
1048
1153
  else if (isJsonObject(payload)
@@ -1060,6 +1165,9 @@ export class V2E2EECoordinator {
1060
1165
  client._clientLog.warn(`P2P Tail 单条消息解密失败,连续性证明仍保留: seq=${seq}`);
1061
1166
  return null;
1062
1167
  }
1168
+ if (publish && client._delivery.isSelfSentP2PMessage?.(plaintext)) {
1169
+ return plaintext;
1170
+ }
1063
1171
  if (publish) {
1064
1172
  const event = client._delivery.p2pAppEventForMessage(plaintext);
1065
1173
  await client._publishAppEvent(event.event, event.payload, source);
@@ -1112,6 +1220,8 @@ export class V2E2EECoordinator {
1112
1220
  payload: payload,
1113
1221
  encrypted: false,
1114
1222
  };
1223
+ if (msg.sender_device_id !== undefined)
1224
+ plaintext.sender_device_id = msg.sender_device_id;
1115
1225
  plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
1116
1226
  attachGatewayProximity(plaintext, msg);
1117
1227
  }
@@ -1223,6 +1333,7 @@ export class V2E2EECoordinator {
1223
1333
  }), true);
1224
1334
  }
1225
1335
  const decrypted = [];
1336
+ let volatileCursor = String(opts?.afterVolatileCursor ?? client._delivery.volatilePullCursor?.() ?? '').trim();
1226
1337
  let nextAfterSeq = opts?.force ? afterSeq : (afterSeq || (ns ? client._seqTracker.getContiguousSeq(ns) : 0));
1227
1338
  let pullGateKey = ns
1228
1339
  ? pullGateKeyForClient(client, 'message.v2.pull', {
@@ -1235,264 +1346,412 @@ export class V2E2EECoordinator {
1235
1346
  let lastAutoAckSeq = 0;
1236
1347
  let pendingAckSeq = deferredForwardAck;
1237
1348
  const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
1238
- while (pageCount < maxPages) {
1239
- pageCount += 1;
1240
- const ackUpToSeq = pendingAckSeq;
1241
- const inlineAckGeneration = ackUpToSeq > 0
1242
- ? client._delivery.captureP2PInlineAckGeneration()
1243
- : null;
1244
- client._clientLog.debug(`message.v2.pull page request: page=${pageCount}, after_seq=${nextAfterSeq}, limit=${limit}, ack_up_to_seq=${ackUpToSeq}, ns=${ns || '<none>'}`);
1245
- const result = await client._callRawV2Rpc('message.v2.pull', {
1246
- after_seq: nextAfterSeq,
1247
- limit,
1248
- ...(opts?.force ? { force: true } : {}),
1249
- ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
1250
- }, pullGateKey);
1251
- if (ackUpToSeq > 0) {
1252
- const actualAckSeq = client._delivery.resolveP2PPullAckSeq(result, ackUpToSeq);
1253
- if (actualAckSeq >= ackUpToSeq) {
1254
- if (inlineAckGeneration !== null) {
1255
- client._delivery.onP2PV2PullAckResult(result, ackUpToSeq, inlineAckGeneration);
1349
+ let pipelineRunning = false;
1350
+ let pipelineError = null;
1351
+ const pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
1352
+ let pipelineProducer = null;
1353
+ let pipelineStopped = false;
1354
+ const startPipeline = (startAfterSeq, startVolatileCursor) => {
1355
+ if (pipelineRunning || pageCount >= maxPages || limit < V2_PULL_MAX_PAGE_LIMIT)
1356
+ return;
1357
+ pipelineRunning = true;
1358
+ pipelineProducer = (async () => {
1359
+ let producerAfterSeq = startAfterSeq;
1360
+ let producerVolatileCursor = startVolatileCursor;
1361
+ for (let produced = pageCount; produced < maxPages; produced += 1) {
1362
+ if (pipelineStopped)
1363
+ break;
1364
+ const requestedVolatileCursor = producerVolatileCursor;
1365
+ const result = await client._callRawV2Rpc('message.v2.pull', {
1366
+ after_seq: producerAfterSeq,
1367
+ limit,
1368
+ ...(producerVolatileCursor ? { after_volatile_cursor: producerVolatileCursor } : {}),
1369
+ ...(opts?.force ? { force: true } : {}),
1370
+ }, pullGateKey);
1371
+ if (pipelineStopped)
1372
+ break;
1373
+ const rawPageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1374
+ const enqueued = await pipelineQueue.put({
1375
+ result,
1376
+ afterSeq: producerAfterSeq,
1377
+ ackUpToSeq: 0,
1378
+ inlineAckGeneration: null,
1379
+ requestedVolatileCursor,
1380
+ weight: rawPageMessages.length,
1381
+ }, rawPageMessages.length);
1382
+ if (!enqueued || pipelineStopped)
1383
+ break;
1384
+ const volatileMessages = client._delivery.volatileMessages?.(rawPageMessages) ?? [];
1385
+ if (volatileMessages.length > 0)
1386
+ break;
1387
+ const messages = deduplicateForwardPageMessages(rawPageMessages, producerAfterSeq);
1388
+ const seqs = messages
1389
+ .map((msg) => Number(msg.seq ?? 0))
1390
+ .filter((seq) => Number.isFinite(seq) && seq > 0);
1391
+ const nextAfter = Math.max(contiguousPullSeq(producerAfterSeq, seqs, result.contiguous_seq), producerAfterSeq);
1392
+ const cursor = isJsonObject(result.cursor)
1393
+ ? result.cursor : null;
1394
+ const shouldContinue = shouldContinueForwardPage({
1395
+ rawCount: messages.length,
1396
+ nextAfterSeq: producerAfterSeq,
1397
+ nextAfter,
1398
+ limit,
1399
+ hasMoreField: Object.prototype.hasOwnProperty.call(result, 'has_more'),
1400
+ hasMore: result.has_more === true,
1401
+ serverHead: safeNonNegativeSeq(cursor?.latest_seq) ?? 0,
1402
+ observedUpper: Math.max(nextAfter, Number(client._pendingP2pPullUpper?.get?.(ns) ?? 0)),
1403
+ pendingAckSeq: 0,
1404
+ });
1405
+ if (!shouldContinue)
1406
+ break;
1407
+ producerAfterSeq = nextAfter;
1408
+ producerVolatileCursor = '';
1409
+ }
1410
+ })().catch((error) => {
1411
+ pipelineError = error;
1412
+ }).finally(() => {
1413
+ pipelineQueue.close();
1414
+ });
1415
+ };
1416
+ const stopPipeline = async () => {
1417
+ pipelineStopped = true;
1418
+ pipelineQueue.close();
1419
+ if (pipelineProducer) {
1420
+ const producer = pipelineProducer;
1421
+ pipelineProducer = null;
1422
+ await producer;
1423
+ }
1424
+ pipelineRunning = false;
1425
+ };
1426
+ try {
1427
+ while (pageCount < maxPages) {
1428
+ let pipelinePage;
1429
+ if (pipelineRunning) {
1430
+ pipelinePage = await pipelineQueue.take();
1431
+ if (!pipelinePage) {
1432
+ pipelineRunning = false;
1433
+ if (pipelineError)
1434
+ throw pipelineError;
1435
+ break;
1256
1436
  }
1257
- pendingAckSeq = 0;
1258
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1259
- this.clearForwardAck(ns, ackUpToSeq);
1260
1437
  }
1261
- else {
1262
- client._clientLog.warn(`message.v2.pull piggyback ACK below target: requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
1263
- await this.confirmForwardP2PAck(ackUpToSeq);
1264
- pendingAckSeq = 0;
1265
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1438
+ pageCount += 1;
1439
+ const requestedVolatileCursor = pipelinePage?.requestedVolatileCursor ?? volatileCursor;
1440
+ const ackUpToSeq = pipelinePage?.ackUpToSeq ?? pendingAckSeq;
1441
+ const inlineAckGeneration = pipelinePage?.inlineAckGeneration ?? (ackUpToSeq > 0
1442
+ ? client._delivery.captureP2PInlineAckGeneration()
1443
+ : null);
1444
+ client._clientLog.debug(`message.v2.pull page request: page=${pageCount}, after_seq=${nextAfterSeq}, limit=${limit}, ack_up_to_seq=${ackUpToSeq}, ns=${ns || '<none>'}`);
1445
+ let result;
1446
+ if (pipelinePage) {
1447
+ nextAfterSeq = pipelinePage.afterSeq;
1448
+ result = pipelinePage.result;
1266
1449
  }
1267
- }
1268
- const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1269
- const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
1270
- const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1271
- const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
1272
- const hasMore = result.has_more === true;
1273
- const serverHead = safeNonNegativeSeq(cursor?.latest_seq) ?? 0;
1274
- client._clientLog.debug(`message.v2.pull page response: page=${pageCount}, raw_count=${messages.length}, stale_count=${Math.max(0, pageMessages.length - messages.length)}, has_more=${String(result.has_more ?? '')}, server_head=${serverHead}, server_ack_seq=${String(result.server_ack_seq ?? '')}`);
1275
- for (const msg of messages) {
1276
- client._logMessageDebug('pull-raw', 'message.v2.pull', 'message.received', msg);
1277
- }
1278
- const seqs = messages
1279
- .map((msg) => Number(msg.seq ?? 0))
1280
- .filter((seq) => Number.isFinite(seq) && seq > 0);
1281
- const pageContigBefore = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
1282
- const pageTrackerSnapshot = ns && typeof client._seqTracker.snapshotNamespace === 'function'
1283
- ? client._seqTracker.snapshotNamespace(ns)
1284
- : null;
1285
- let pageMaxSeq = nextAfterSeq;
1286
- if (seqs.length > 0) {
1287
- pageMaxSeq = Math.max(...seqs);
1288
- }
1289
- const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
1290
- const deferredKeyFetches = new Map();
1291
- let blockedSeq = 0;
1292
- const pageDecryptedBefore = decrypted.length;
1293
- for (const msg of messages) {
1294
- const seq = Number(msg.seq ?? 0);
1295
- if (!Number.isFinite(seq) || seq <= 0)
1296
- continue;
1297
- const version = String(msg.version ?? 'v2');
1298
- if (version === 'v1') {
1299
- const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
1300
- const legacyPayload = legacy.payload !== undefined ? legacy.payload : msg.payload;
1301
- const payloadType = isJsonObject(legacyPayload)
1302
- ? String(legacyPayload.type ?? '').trim()
1303
- : '';
1304
- if (legacyPayload !== undefined && legacyPayload !== null
1305
- && !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
1306
- const v1Msg = {
1307
- message_id: String(msg.message_id ?? ''),
1308
- from: String(msg.from_aid ?? ''),
1309
- to: String(legacy.to ?? client._aid ?? ''),
1310
- seq: msg.seq,
1311
- type: String(msg.type ?? ''),
1312
- timestamp: msg.t_server,
1313
- payload: legacyPayload,
1314
- encrypted: false,
1315
- };
1316
- attachGatewayProximity(v1Msg, msg);
1317
- const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
1318
- if (ns) {
1319
- await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
1320
- }
1321
- else {
1322
- await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull');
1323
- }
1324
- decrypted.push(v1Msg);
1325
- client._clientLog.debug(`message.v2.pull plaintext V1 delivered: seq=${seq}, ns=${ns || '<none>'}`);
1450
+ else {
1451
+ try {
1452
+ result = await client._callRawV2Rpc('message.v2.pull', {
1453
+ after_seq: nextAfterSeq,
1454
+ limit,
1455
+ ...(volatileCursor ? { after_volatile_cursor: volatileCursor } : {}),
1456
+ ...(opts?.force ? { force: true } : {}),
1457
+ ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
1458
+ }, pullGateKey);
1326
1459
  }
1327
- else if (isJsonObject(legacyPayload)
1328
- && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1329
- const deferStatus = {};
1330
- const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(legacyPayload) }, true, true, true, true, deferStatus, true, 'pull');
1331
- if (deferStatus.deferred) {
1332
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1333
- if (deferStatus.fromAid) {
1334
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1335
- deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
1336
- }
1460
+ catch (exc) {
1461
+ if (!volatileCursor || !String(exc instanceof Error ? exc.message : exc).toLowerCase().includes('volatile cursor epoch mismatch')) {
1462
+ throw exc;
1337
1463
  }
1338
- if (plaintext) {
1339
- if (ns)
1340
- await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1341
- else
1342
- await client._publishAppEvent('message.received', plaintext, 'pull');
1343
- decrypted.push(plaintext);
1464
+ client._delivery.resetVolatilePullCursor?.();
1465
+ volatileCursor = '';
1466
+ pageCount -= 1;
1467
+ continue;
1468
+ }
1469
+ }
1470
+ if (ackUpToSeq > 0) {
1471
+ const actualAckSeq = client._delivery.resolveP2PPullAckSeq(result, ackUpToSeq);
1472
+ if (actualAckSeq >= ackUpToSeq) {
1473
+ if (inlineAckGeneration !== null) {
1474
+ client._delivery.onP2PV2PullAckResult(result, ackUpToSeq, inlineAckGeneration);
1344
1475
  }
1476
+ pendingAckSeq = 0;
1477
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1478
+ this.clearForwardAck(ns, ackUpToSeq);
1345
1479
  }
1346
1480
  else {
1347
- client._clientLog.debug(`message.v2.pull skipping V1 envelope seq=${seq} payload_type=${payloadType || '<none>'} (V1 E2EE removed)`);
1481
+ client._clientLog.warn(`message.v2.pull piggyback ACK below target: requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
1482
+ await this.confirmForwardP2PAck(ackUpToSeq);
1483
+ pendingAckSeq = 0;
1484
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1348
1485
  }
1349
- continue;
1350
1486
  }
1351
- if (version !== 'v2') {
1352
- client._clientLog.debug(`message.v2.pull skipping non-V2 row seq=${seq} version=${String(msg.version ?? '')}`);
1353
- continue;
1354
- }
1355
- const spkId = String(msg.spk_id ?? '');
1356
- if (spkId && client._v2Session && !client._v2Session.isCurrentSPK(spkId)) {
1357
- client._v2Session.trackOldSPKMaxSeq(spkId, seq);
1487
+ const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1488
+ const volatileMessages = client._delivery.volatileMessages?.(pageMessages) ?? [];
1489
+ if (volatileMessages.length > 0) {
1490
+ decrypted.push(...await this.publishV2VolatileMessages(volatileMessages, 'pull', true));
1491
+ volatileCursor = String(client._delivery.volatilePullCursor?.() ?? '').trim();
1358
1492
  }
1359
- const deferStatus = {};
1360
- const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
1361
- if (deferStatus.deferred) {
1362
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1493
+ const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
1494
+ const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1495
+ const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
1496
+ const hasMore = result.has_more === true;
1497
+ const volatileHasMore = result.volatile_has_more === true;
1498
+ const serverHead = safeNonNegativeSeq(cursor?.latest_seq) ?? 0;
1499
+ client._clientLog.debug(`message.v2.pull page response: page=${pageCount}, raw_count=${messages.length}, stale_count=${Math.max(0, pageMessages.length - messages.length)}, has_more=${String(result.has_more ?? '')}, server_head=${serverHead}, server_ack_seq=${String(result.server_ack_seq ?? '')}`);
1500
+ for (const msg of messages) {
1501
+ client._logMessageDebug('pull-raw', 'message.v2.pull', 'message.received', msg);
1363
1502
  }
1364
- if (deferStatus.deferred && deferStatus.fromAid) {
1365
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1366
- deferredKeyFetches.set(key, {
1367
- fromAid: deferStatus.fromAid,
1368
- senderDeviceId: deferStatus.senderDeviceId ?? '',
1369
- groupId: deferStatus.groupId ?? '',
1370
- });
1503
+ const seqs = messages
1504
+ .map((msg) => Number(msg.seq ?? 0))
1505
+ .filter((seq) => Number.isFinite(seq) && seq > 0);
1506
+ const pageContigBefore = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
1507
+ const pageTrackerSnapshot = ns && typeof client._seqTracker.snapshotNamespace === 'function'
1508
+ ? client._seqTracker.snapshotNamespace(ns)
1509
+ : null;
1510
+ let pageMaxSeq = nextAfterSeq;
1511
+ if (seqs.length > 0) {
1512
+ pageMaxSeq = Math.max(...seqs);
1371
1513
  }
1372
- if (plaintext === null) {
1373
- client._clientLog.debug(`message.v2.pull decrypt returned null: seq=${seq}, ns=${ns || '<none>'}`);
1374
- continue;
1514
+ const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
1515
+ const deferredKeyFetches = new Map();
1516
+ let blockedSeq = 0;
1517
+ const pageDecryptedBefore = decrypted.length;
1518
+ for (const msg of messages) {
1519
+ const seq = Number(msg.seq ?? 0);
1520
+ if (!Number.isFinite(seq) || seq <= 0)
1521
+ continue;
1522
+ const version = String(msg.version ?? 'v2');
1523
+ if (version === 'v1') {
1524
+ const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
1525
+ const legacyPayload = legacy.payload !== undefined ? legacy.payload : msg.payload;
1526
+ const payloadType = isJsonObject(legacyPayload)
1527
+ ? String(legacyPayload.type ?? '').trim()
1528
+ : '';
1529
+ if (legacyPayload !== undefined && legacyPayload !== null
1530
+ && !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
1531
+ const v1Msg = {
1532
+ message_id: String(msg.message_id ?? ''),
1533
+ from: String(msg.from_aid ?? ''),
1534
+ to: String(legacy.to ?? client._aid ?? ''),
1535
+ seq: msg.seq,
1536
+ type: String(msg.type ?? ''),
1537
+ timestamp: msg.t_server,
1538
+ payload: legacyPayload,
1539
+ encrypted: false,
1540
+ };
1541
+ const direction = String(msg.direction ?? '').trim();
1542
+ if (direction)
1543
+ v1Msg.direction = direction;
1544
+ else if (String(msg.from_aid ?? '').trim() && String(msg.from_aid ?? '').trim() !== String(client._aid ?? '').trim()) {
1545
+ v1Msg.direction = 'inbound';
1546
+ }
1547
+ if (msg.sender_device_id !== undefined)
1548
+ v1Msg.sender_device_id = msg.sender_device_id;
1549
+ attachGatewayProximity(v1Msg, msg);
1550
+ const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
1551
+ if (ns) {
1552
+ await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
1553
+ }
1554
+ else {
1555
+ await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull');
1556
+ }
1557
+ decrypted.push(v1Msg);
1558
+ client._clientLog.debug(`message.v2.pull plaintext V1 delivered: seq=${seq}, ns=${ns || '<none>'}`);
1559
+ }
1560
+ else if (isJsonObject(legacyPayload)
1561
+ && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1562
+ const deferStatus = {};
1563
+ const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(legacyPayload) }, true, true, true, true, deferStatus, true, 'pull');
1564
+ if (deferStatus.deferred) {
1565
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1566
+ if (deferStatus.fromAid) {
1567
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1568
+ deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
1569
+ }
1570
+ }
1571
+ if (plaintext) {
1572
+ if (ns)
1573
+ await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1574
+ else
1575
+ await client._publishAppEvent('message.received', plaintext, 'pull');
1576
+ decrypted.push(plaintext);
1577
+ }
1578
+ }
1579
+ else {
1580
+ client._clientLog.debug(`message.v2.pull skipping V1 envelope seq=${seq} payload_type=${payloadType || '<none>'} (V1 E2EE removed)`);
1581
+ }
1582
+ continue;
1583
+ }
1584
+ if (version !== 'v2') {
1585
+ client._clientLog.debug(`message.v2.pull skipping non-V2 row seq=${seq} version=${String(msg.version ?? '')}`);
1586
+ continue;
1587
+ }
1588
+ const spkId = String(msg.spk_id ?? '');
1589
+ if (spkId && client._v2Session && !client._v2Session.isCurrentSPK(spkId)) {
1590
+ client._v2Session.trackOldSPKMaxSeq(spkId, seq);
1591
+ }
1592
+ const deferStatus = {};
1593
+ const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
1594
+ if (deferStatus.deferred) {
1595
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1596
+ }
1597
+ if (deferStatus.deferred && deferStatus.fromAid) {
1598
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1599
+ deferredKeyFetches.set(key, {
1600
+ fromAid: deferStatus.fromAid,
1601
+ senderDeviceId: deferStatus.senderDeviceId ?? '',
1602
+ groupId: deferStatus.groupId ?? '',
1603
+ });
1604
+ }
1605
+ if (plaintext === null) {
1606
+ client._clientLog.debug(`message.v2.pull decrypt returned null: seq=${seq}, ns=${ns || '<none>'}`);
1607
+ continue;
1608
+ }
1609
+ if (ns) {
1610
+ await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1611
+ }
1612
+ else {
1613
+ await client._publishAppEvent('message.received', plaintext, 'pull');
1614
+ }
1615
+ decrypted.push(plaintext);
1616
+ client._logMessageDebug('decrypt-ok', 'message.v2.pull', 'message.received', plaintext);
1375
1617
  }
1618
+ const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
1619
+ const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
1620
+ const serverAckSeq = parsedServerAckSeq ?? 0;
1621
+ const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
1376
1622
  if (ns) {
1377
- await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1378
- }
1379
- else {
1380
- await client._publishAppEvent('message.received', plaintext, 'pull');
1381
- }
1382
- decrypted.push(plaintext);
1383
- client._logMessageDebug('decrypt-ok', 'message.v2.pull', 'message.received', plaintext);
1384
- }
1385
- const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
1386
- const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
1387
- const serverAckSeq = parsedServerAckSeq ?? 0;
1388
- const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
1389
- if (ns) {
1390
- const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, serverAckSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
1391
- let ackSeq = pageContigBefore;
1392
- let contigAdvanced = false;
1393
- try {
1394
- if (commitTarget > pageContigBefore) {
1395
- client._seqTracker.forceContiguousSeq(ns, commitTarget);
1396
- if (retentionFloor > pageContigBefore) {
1397
- client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${pageContigBefore} -> retention_floor_seq=${retentionFloor}`);
1623
+ const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, serverAckSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
1624
+ let ackSeq = pageContigBefore;
1625
+ let contigAdvanced = false;
1626
+ try {
1627
+ if (commitTarget > pageContigBefore) {
1628
+ client._seqTracker.forceContiguousSeq(ns, commitTarget);
1629
+ if (retentionFloor > pageContigBefore) {
1630
+ client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${pageContigBefore} -> retention_floor_seq=${retentionFloor}`);
1631
+ }
1632
+ if (deferredServerCursor > pageContigBefore && pageCount === 1) {
1633
+ client._clientLog.info(`message.v2.pull restore local A from deferred server ACK cursor: ns=${ns}, contiguous=${pageContigBefore} -> server_ack_seq=${deferredServerCursor}`);
1634
+ }
1635
+ if (seqs.length > 0) {
1636
+ client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
1637
+ }
1398
1638
  }
1399
- if (deferredServerCursor > pageContigBefore && pageCount === 1) {
1400
- client._clientLog.info(`message.v2.pull restore local A from deferred server ACK cursor: ns=${ns}, contiguous=${pageContigBefore} -> server_ack_seq=${deferredServerCursor}`);
1639
+ ackSeq = client._seqTracker.getContiguousSeq(ns);
1640
+ contigAdvanced = ackSeq !== pageContigBefore;
1641
+ if (contigAdvanced) {
1642
+ await client._commitSeqTrackerState(ns);
1643
+ if (pageCount === 1 && deferredServerCursor > 0)
1644
+ this.clearForwardCursor(ns, ackSeq);
1401
1645
  }
1402
- if (seqs.length > 0) {
1403
- client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
1646
+ }
1647
+ catch (exc) {
1648
+ if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
1649
+ client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
1404
1650
  }
1651
+ client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
1652
+ throw exc;
1405
1653
  }
1406
- ackSeq = client._seqTracker.getContiguousSeq(ns);
1407
- contigAdvanced = ackSeq !== pageContigBefore;
1408
- if (contigAdvanced) {
1409
- await client._commitSeqTrackerState(ns);
1410
- if (pageCount === 1 && deferredServerCursor > 0)
1411
- this.clearForwardCursor(ns, ackSeq);
1654
+ if (contigAdvanced)
1655
+ await client._drainOrderedMessages(ns, undefined, false, false, 'pull');
1656
+ for (const fetch of deferredKeyFetches.values()) {
1657
+ this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1412
1658
  }
1413
- }
1414
- catch (exc) {
1415
- if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
1416
- client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
1659
+ if (parsedServerAckSeq !== null && serverAckSeq > ackSeq) {
1660
+ this.recordForwardCursor(ns, serverAckSeq, ackSeq);
1661
+ }
1662
+ // 无本页服务端水位时,只有本页成功提交了比已知服务端更高的 A
1663
+ // 才新增 ACK 义务。空页不能仅因本地已有 A 而重复 ACK。
1664
+ const knownServerAckSeq = hasServerAckSeq
1665
+ ? serverAckSeq
1666
+ : (pageCount === 1 ? deferredServerCursor : 0);
1667
+ const ackNeeded = ackSeq > 0
1668
+ && ackSeq > lastAutoAckSeq
1669
+ && ((hasServerAckSeq && ackSeq > serverAckSeq)
1670
+ || (contigAdvanced && ackSeq > knownServerAckSeq));
1671
+ if (ackNeeded) {
1672
+ this.recordForwardAck(ns, ackSeq);
1673
+ const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
1674
+ const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq;
1675
+ if (canContinuePage && !pipelineRunning) {
1676
+ pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
1677
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1678
+ client._clientLog.debug(`message.v2.pull queued piggyback auto-ack: ns=${ns}, ack_seq=${pendingAckSeq}, raw_count=${messages.length}`);
1679
+ }
1680
+ else {
1681
+ client._clientLog.debug(`message.v2.pull sending terminal auto-ack: ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
1682
+ await this.confirmForwardP2PAck(ackSeq);
1683
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1684
+ }
1417
1685
  }
1418
- client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
1419
- throw exc;
1420
- }
1421
- if (contigAdvanced)
1422
- await client._drainOrderedMessages(ns, undefined, false, false, 'pull');
1423
- for (const fetch of deferredKeyFetches.values()) {
1424
- this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1425
- }
1426
- if (parsedServerAckSeq !== null && serverAckSeq > ackSeq) {
1427
- this.recordForwardCursor(ns, serverAckSeq, ackSeq);
1428
1686
  }
1429
- // 无本页服务端水位时,只有本页成功提交了比已知服务端更高的 A
1430
- // 才新增 ACK 义务。空页不能仅因本地已有 A 而重复 ACK。
1431
- const knownServerAckSeq = hasServerAckSeq
1432
- ? serverAckSeq
1433
- : (pageCount === 1 ? deferredServerCursor : 0);
1434
- const ackNeeded = ackSeq > 0
1435
- && ackSeq > lastAutoAckSeq
1436
- && ((hasServerAckSeq && ackSeq > serverAckSeq)
1437
- || (contigAdvanced && ackSeq > knownServerAckSeq));
1438
- if (ackNeeded) {
1439
- this.recordForwardAck(ns, ackSeq);
1440
- const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
1441
- const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq;
1442
- if (canContinuePage) {
1443
- pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
1444
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1445
- client._clientLog.debug(`message.v2.pull queued piggyback auto-ack: ns=${ns}, ack_seq=${pendingAckSeq}, raw_count=${messages.length}`);
1687
+ if (!ns) {
1688
+ for (const fetch of deferredKeyFetches.values()) {
1689
+ this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1446
1690
  }
1447
- else {
1448
- client._clientLog.debug(`message.v2.pull sending terminal auto-ack: ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
1449
- await this.confirmForwardP2PAck(ackSeq);
1450
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1691
+ }
1692
+ if (ns)
1693
+ client._delivery.syncProgress(ns, {
1694
+ rawCount: messages.length,
1695
+ pulledCount: decrypted.length - pageDecryptedBefore,
1696
+ remaining: result.remaining,
1697
+ hasMore: hasMoreField ? hasMore : undefined,
1698
+ currentSeq: client._seqTracker.getContiguousSeq(ns),
1699
+ targetSeq: serverHead,
1700
+ });
1701
+ const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
1702
+ const rawCount = messages.length;
1703
+ if (blockedSeq > 0)
1704
+ break;
1705
+ const shouldContinuePersistent = shouldContinueForwardPage({
1706
+ rawCount,
1707
+ nextAfterSeq,
1708
+ nextAfter,
1709
+ limit,
1710
+ hasMoreField,
1711
+ hasMore,
1712
+ serverHead,
1713
+ observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._pendingP2pPullUpper?.get?.(ns) ?? 0)),
1714
+ pendingAckSeq,
1715
+ });
1716
+ const shouldContinueForPipeline = shouldContinueForwardPage({
1717
+ rawCount,
1718
+ nextAfterSeq,
1719
+ nextAfter,
1720
+ limit,
1721
+ hasMoreField,
1722
+ hasMore,
1723
+ serverHead,
1724
+ observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._pendingP2pPullUpper?.get?.(ns) ?? 0)),
1725
+ pendingAckSeq: 0,
1726
+ });
1727
+ const shouldContinueVolatile = (hasMore || volatileHasMore)
1728
+ && volatileMessages.length > 0
1729
+ && Boolean(volatileCursor)
1730
+ && volatileCursor !== requestedVolatileCursor;
1731
+ if (!shouldContinuePersistent && !shouldContinueVolatile)
1732
+ break;
1733
+ if (shouldContinueForPipeline && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
1734
+ if (pendingAckSeq > 0) {
1735
+ await this.confirmForwardP2PAck(pendingAckSeq);
1736
+ pendingAckSeq = 0;
1451
1737
  }
1738
+ startPipeline(nextAfter, volatileCursor);
1452
1739
  }
1453
- }
1454
- if (!ns) {
1455
- for (const fetch of deferredKeyFetches.values()) {
1456
- this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1740
+ if (!pipelineRunning && pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
1741
+ const nextKey = pullGateKeyForClient(client, 'message.v2.pull', {
1742
+ after_seq: nextAfter, force: opts?.force === true, limit,
1743
+ }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
1744
+ await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
1745
+ pullGateKey = nextKey;
1457
1746
  }
1747
+ const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
1748
+ if (tailDelayMs > 0)
1749
+ await v2Sleep(tailDelayMs);
1750
+ nextAfterSeq = nextAfter;
1458
1751
  }
1459
- if (ns)
1460
- client._delivery.syncProgress(ns, {
1461
- rawCount: messages.length,
1462
- pulledCount: decrypted.length - pageDecryptedBefore,
1463
- remaining: result.remaining,
1464
- hasMore: hasMoreField ? hasMore : undefined,
1465
- currentSeq: client._seqTracker.getContiguousSeq(ns),
1466
- targetSeq: serverHead,
1467
- });
1468
- const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
1469
- const rawCount = messages.length;
1470
- if (blockedSeq > 0)
1471
- break;
1472
- const shouldContinue = shouldContinueForwardPage({
1473
- rawCount,
1474
- nextAfterSeq,
1475
- nextAfter,
1476
- limit,
1477
- hasMoreField,
1478
- hasMore,
1479
- serverHead,
1480
- observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._pendingP2pPullUpper?.get?.(ns) ?? 0)),
1481
- pendingAckSeq,
1482
- });
1483
- if (!shouldContinue)
1484
- break;
1485
- if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
1486
- const nextKey = pullGateKeyForClient(client, 'message.v2.pull', {
1487
- after_seq: nextAfter, force: opts?.force === true, limit,
1488
- }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
1489
- await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
1490
- pullGateKey = nextKey;
1491
- }
1492
- const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
1493
- if (tailDelayMs > 0)
1494
- await v2Sleep(tailDelayMs);
1495
- nextAfterSeq = nextAfter;
1752
+ }
1753
+ finally {
1754
+ await stopPipeline();
1496
1755
  }
1497
1756
  if (pageCount >= maxPages) {
1498
1757
  client._clientLog.warn(`message.v2.pull reached max_pages=${maxPages} after_seq=${nextAfterSeq}`);
@@ -1518,6 +1777,73 @@ export class V2E2EECoordinator {
1518
1777
  throw error;
1519
1778
  }
1520
1779
  }
1780
+ async publishV2VolatileMessages(messages, source, advanceCursor) {
1781
+ const client = this.client;
1782
+ if (!client._v2Session)
1783
+ return [];
1784
+ const processed = [];
1785
+ const plaintextMessages = [];
1786
+ let complete = true;
1787
+ for (const rawMessage of messages) {
1788
+ const claim = client._delivery.beginVolatileDelivery(rawMessage);
1789
+ if (claim === 'seen') {
1790
+ processed.push(rawMessage);
1791
+ continue;
1792
+ }
1793
+ if (claim === 'inflight') {
1794
+ complete = false;
1795
+ continue;
1796
+ }
1797
+ let plaintext;
1798
+ try {
1799
+ if (this.isEncryptedPushMessage(rawMessage)) {
1800
+ plaintext = await client._decryptV2Message(rawMessage, true, true, true, true, undefined, false, source);
1801
+ }
1802
+ else {
1803
+ const legacy = isJsonObject(rawMessage.legacy_v1)
1804
+ ? rawMessage.legacy_v1
1805
+ : {};
1806
+ plaintext = {
1807
+ message_id: String(rawMessage.message_id ?? ''),
1808
+ from: String(rawMessage.from_aid ?? rawMessage.from ?? ''),
1809
+ to: String(legacy.to ?? rawMessage.to ?? client._aid ?? ''),
1810
+ type: String(rawMessage.type ?? ''),
1811
+ timestamp: rawMessage.t_server ?? rawMessage.timestamp,
1812
+ payload: legacy.payload !== undefined ? legacy.payload : rawMessage.payload,
1813
+ encrypted: false,
1814
+ };
1815
+ for (const key of ['context', 'protected_headers', 'headers', 'client_signature']) {
1816
+ if (Object.prototype.hasOwnProperty.call(rawMessage, key))
1817
+ plaintext[key] = rawMessage[key];
1818
+ }
1819
+ if (rawMessage.sender_device_id !== undefined)
1820
+ plaintext.sender_device_id = rawMessage.sender_device_id;
1821
+ attachGatewayProximity(plaintext, rawMessage);
1822
+ }
1823
+ if (!plaintext) {
1824
+ complete = false;
1825
+ continue;
1826
+ }
1827
+ const publicMessage = client._delivery.publicVolatileMessage(plaintext);
1828
+ for (const key of ['storage', 'volatile_cursor', 'created_at', 'expires_at', 'persist']) {
1829
+ if (Object.prototype.hasOwnProperty.call(rawMessage, key))
1830
+ publicMessage[key] = rawMessage[key];
1831
+ }
1832
+ const appEvent = client._delivery.p2pAppEventForMessage(publicMessage);
1833
+ await client._publishAppEvent(appEvent.event, appEvent.payload, source);
1834
+ client._delivery.markVolatileSeen(rawMessage);
1835
+ processed.push(rawMessage);
1836
+ plaintextMessages.push(publicMessage);
1837
+ }
1838
+ finally {
1839
+ client._delivery.finishVolatileDelivery(rawMessage);
1840
+ }
1841
+ }
1842
+ if (processed.length > 0) {
1843
+ await client._delivery.ackVolatileMessages(processed, true, advanceCursor && complete && processed.length === messages.length);
1844
+ }
1845
+ return plaintextMessages;
1846
+ }
1521
1847
  async confirmForwardP2PAck(upToSeq) {
1522
1848
  const ns = this.client._aid ? `p2p:${this.client._aid}` : '';
1523
1849
  try {
@@ -1533,28 +1859,41 @@ export class V2E2EECoordinator {
1533
1859
  throw exc;
1534
1860
  }
1535
1861
  }
1536
- async ackV2(upToSeq) {
1537
- return this.ackV2WithConfirmation(upToSeq, false);
1862
+ async ackV2(upToSeq, opts) {
1863
+ return this.ackV2WithConfirmation(upToSeq, false, opts);
1538
1864
  }
1539
1865
  async ackV2ForForward(upToSeq) {
1540
1866
  return this.ackV2WithConfirmation(upToSeq, true);
1541
1867
  }
1542
- async ackV2WithConfirmation(upToSeq, requireServerWatermark) {
1868
+ async ackV2WithConfirmation(upToSeq, requireServerWatermark, opts) {
1543
1869
  const client = this.client;
1544
1870
  const ns = client._aid ? `p2p:${client._aid}` : '';
1545
- let seq = Number(upToSeq ?? (ns ? client._seqTracker.getContiguousSeq(ns) : 0));
1546
- if (!Number.isFinite(seq) || seq <= 0) {
1871
+ const volatileCursors = Array.isArray(opts?.volatileCursors) ? opts?.volatileCursors : [];
1872
+ const volatileIds = Array.isArray(opts?.volatileIds) ? opts?.volatileIds : [];
1873
+ let seq = Number(upToSeq ?? ((volatileCursors.length > 0 || volatileIds.length > 0)
1874
+ ? 0
1875
+ : (ns ? client._seqTracker.getContiguousSeq(ns) : 0)));
1876
+ if ((!Number.isFinite(seq) || seq <= 0) && volatileCursors.length === 0 && volatileIds.length === 0) {
1547
1877
  client._clientLog.debug(`message.v2.ack skipped: ns=${ns || '<none>'}, up_to_seq=${String(upToSeq ?? '')}`);
1548
1878
  return { acked: 0 };
1549
1879
  }
1550
- seq = client._clampAckSeq('message.v2.ack', 'up_to_seq', ns, seq);
1551
- if (seq <= 0)
1880
+ if (Number.isFinite(seq) && seq > 0) {
1881
+ seq = client._clampAckSeq('message.v2.ack', 'up_to_seq', ns, seq);
1882
+ }
1883
+ else {
1884
+ seq = 0;
1885
+ }
1886
+ if (seq <= 0 && volatileCursors.length === 0 && volatileIds.length === 0)
1552
1887
  return { acked: 0 };
1553
1888
  const inlineAckGeneration = client._delivery.captureP2PInlineAckGeneration();
1554
1889
  const v2SessionAtStart = client._v2Session;
1555
1890
  const v2RuntimeGenerationAtStart = Number(client._v2RuntimeGeneration ?? 0);
1556
1891
  client._clientLog.debug(`message.v2.ack send: ns=${ns || '<none>'}, up_to_seq=${seq}`);
1557
- const raw = await client._callRawV2Rpc('message.v2.ack', { up_to_seq: seq });
1892
+ const raw = await client._callRawV2Rpc('message.v2.ack', {
1893
+ ...(seq > 0 ? { up_to_seq: seq } : {}),
1894
+ ...(volatileCursors.length > 0 ? { volatile_cursors: volatileCursors } : {}),
1895
+ ...(volatileIds.length > 0 ? { volatile_ids: volatileIds } : {}),
1896
+ });
1558
1897
  const result = isJsonObject(raw)
1559
1898
  ? { ...raw }
1560
1899
  : { result: raw };
@@ -1566,7 +1905,7 @@ export class V2E2EECoordinator {
1566
1905
  }
1567
1906
  result.ack_seq = actualAckSeq;
1568
1907
  result.success = true;
1569
- if (inlineAckGeneration !== null) {
1908
+ if (seq > 0 && inlineAckGeneration !== null) {
1570
1909
  client._delivery.onP2PV2AckSucceeded(actualAckSeq, inlineAckGeneration);
1571
1910
  }
1572
1911
  if (v2SessionAtStart
@@ -1664,7 +2003,6 @@ export class V2E2EECoordinator {
1664
2003
  let epoch = 0;
1665
2004
  let stateCommitment = { state_version: 0, state_hash: '', state_chain: '' };
1666
2005
  let wrapPolicy = normalizeV2WrapPolicy(undefined);
1667
- let usedCachedBootstrap = false;
1668
2006
  const cached = useCache ? this.getBootstrapCacheEntry(cacheKey) : undefined;
1669
2007
  if (cached && Date.now() - Number(cached.cachedAt ?? 0) < V2_BOOTSTRAP_TTL_MS) {
1670
2008
  allDevices = (cached.devices ?? []);
@@ -1672,7 +2010,6 @@ export class V2E2EECoordinator {
1672
2010
  epoch = Number(cached.epoch ?? 0) || 0;
1673
2011
  stateCommitment = cached.stateCommitment ?? stateCommitment;
1674
2012
  wrapPolicy = cached.wrapPolicy ?? wrapPolicy;
1675
- usedCachedBootstrap = true;
1676
2013
  client._clientLog.debug(`group.v2.bootstrap cache hit: group=${groupId}, devices=${allDevices.length}, audit=${auditRecipientsRaw.length}, epoch=${epoch}, state_version=${stateCommitment.state_version}`);
1677
2014
  }
1678
2015
  else {
@@ -1736,13 +2073,10 @@ export class V2E2EECoordinator {
1736
2073
  targets.push(target);
1737
2074
  }
1738
2075
  if (targets.length === 0) {
1739
- if (usedCachedBootstrap) {
1740
- throw new E2EEError(`V2 group: no target devices for group ${groupId}; bootstrap may be stale`, {
1741
- code: V2_GROUP_STALE_BOOTSTRAP_CODE,
1742
- data: { expected_peer_aids: [] },
1743
- });
1744
- }
1745
- throw new E2EEError(`V2 group: no target devices for group ${groupId}`);
2076
+ throw new E2EEError(`V2 group: no target devices for group ${groupId}; bootstrap may be stale`, {
2077
+ code: V2_GROUP_STALE_BOOTSTRAP_CODE,
2078
+ data: { expected_peer_aids: [] },
2079
+ });
1746
2080
  }
1747
2081
  for (const dev of auditRecipientsRaw) {
1748
2082
  const target = await client._v2BuildTargetFromDevice({
@@ -1761,13 +2095,10 @@ export class V2E2EECoordinator {
1761
2095
  targetSet = this.cloneGroupTargetSet(targetSet);
1762
2096
  }
1763
2097
  if (targetSet.targets.length === 0) {
1764
- if (usedCachedBootstrap) {
1765
- throw new E2EEError(`V2 group: no target devices for group ${groupId}; bootstrap may be stale`, {
1766
- code: V2_GROUP_STALE_BOOTSTRAP_CODE,
1767
- data: { expected_peer_aids: [] },
1768
- });
1769
- }
1770
- throw new E2EEError(`V2 group: no target devices for group ${groupId}`);
2098
+ throw new E2EEError(`V2 group: no target devices for group ${groupId}; bootstrap may be stale`, {
2099
+ code: V2_GROUP_STALE_BOOTSTRAP_CODE,
2100
+ data: { expected_peer_aids: [] },
2101
+ });
1771
2102
  }
1772
2103
  const envelope = encryptGroupMessage(session.getSenderIdentity(), groupId, epoch, targetSet.targets, opts.payload, {
1773
2104
  messageId: opts.messageId,
@@ -1958,91 +2289,208 @@ export class V2E2EECoordinator {
1958
2289
  let lastAutoAckSeq = 0;
1959
2290
  let pendingAckSeq = deferredForwardAck;
1960
2291
  const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
1961
- while (pageCount < maxPages) {
1962
- pageCount += 1;
1963
- const ackUpToSeq = ownsCursor ? pendingAckSeq : 0;
1964
- const inlineAckGeneration = ackUpToSeq > 0
1965
- ? client._delivery.captureGroupInlineAckGeneration()
1966
- : null;
1967
- client._clientLog.debug(`group.v2.pull page request: group=${gid}, page=${pageCount}, after_seq=${nextAfterSeq}, limit=${limit}, ack_up_to_seq=${ackUpToSeq}, ns=${ns}`);
1968
- const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
1969
- group_id: wireGroupId,
1970
- after_seq: nextAfterSeq,
1971
- limit,
1972
- ...cursorParams,
1973
- ...(opts?.force ? { force: true } : {}),
1974
- ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
1975
- }, groupAid), pullGateKey);
1976
- if (ackUpToSeq > 0) {
1977
- const actualAckSeq = client._delivery.resolveGroupPullAckSeq(result, ackUpToSeq);
1978
- if (actualAckSeq >= ackUpToSeq) {
1979
- if (inlineAckGeneration !== null) {
1980
- client._delivery.onGroupV2PullAckResult(gid, result, ackUpToSeq, inlineAckGeneration);
2292
+ let pipelineRunning = false;
2293
+ let pipelineError = null;
2294
+ const pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
2295
+ let pipelineProducer = null;
2296
+ let pipelineStopped = false;
2297
+ const startPipeline = (startAfterSeq) => {
2298
+ if (pipelineRunning || pageCount >= maxPages || limit < V2_PULL_MAX_PAGE_LIMIT || !ownsCursor)
2299
+ return;
2300
+ pipelineRunning = true;
2301
+ pipelineProducer = (async () => {
2302
+ let producerAfterSeq = startAfterSeq;
2303
+ for (let produced = pageCount; produced < maxPages; produced += 1) {
2304
+ if (pipelineStopped)
2305
+ break;
2306
+ const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2307
+ group_id: wireGroupId,
2308
+ after_seq: producerAfterSeq,
2309
+ limit,
2310
+ ...cursorParams,
2311
+ ...(opts?.force ? { force: true } : {}),
2312
+ }, groupAid), pullGateKey);
2313
+ if (pipelineStopped)
2314
+ break;
2315
+ const pageMessages = (Array.isArray(result.messages) ? result.messages : []);
2316
+ const enqueued = await pipelineQueue.put({
2317
+ result,
2318
+ afterSeq: producerAfterSeq,
2319
+ ackUpToSeq: 0,
2320
+ inlineAckGeneration: null,
2321
+ requestedVolatileCursor: '',
2322
+ weight: pageMessages.length,
2323
+ }, pageMessages.length);
2324
+ if (!enqueued || pipelineStopped)
2325
+ break;
2326
+ const messages = deduplicateForwardPageMessages(pageMessages, producerAfterSeq);
2327
+ const seqs = messages
2328
+ .map((msg) => Number(msg.seq ?? 0))
2329
+ .filter((seq) => Number.isFinite(seq) && seq > 0);
2330
+ const nextAfter = Math.max(contiguousPullSeq(producerAfterSeq, seqs, result.contiguous_seq), producerAfterSeq);
2331
+ const cursor = isJsonObject(result.cursor)
2332
+ ? result.cursor : null;
2333
+ const shouldContinue = shouldContinueForwardPage({
2334
+ rawCount: messages.length,
2335
+ nextAfterSeq: producerAfterSeq,
2336
+ nextAfter,
2337
+ limit,
2338
+ hasMoreField: Object.prototype.hasOwnProperty.call(result, 'has_more'),
2339
+ hasMore: result.has_more === true,
2340
+ serverHead: Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0),
2341
+ observedUpper: Math.max(nextAfter, client._seqTracker.getMaxSeenSeq(ns)),
2342
+ pendingAckSeq: 0,
2343
+ });
2344
+ if (!shouldContinue)
2345
+ break;
2346
+ producerAfterSeq = nextAfter;
2347
+ }
2348
+ })().catch((error) => {
2349
+ pipelineError = error;
2350
+ }).finally(() => {
2351
+ pipelineQueue.close();
2352
+ });
2353
+ };
2354
+ const stopPipeline = async () => {
2355
+ pipelineStopped = true;
2356
+ pipelineQueue.close();
2357
+ if (pipelineProducer) {
2358
+ const producer = pipelineProducer;
2359
+ pipelineProducer = null;
2360
+ await producer;
2361
+ }
2362
+ pipelineRunning = false;
2363
+ };
2364
+ try {
2365
+ while (pageCount < maxPages) {
2366
+ let pipelinePage;
2367
+ if (pipelineRunning) {
2368
+ pipelinePage = await pipelineQueue.take();
2369
+ if (!pipelinePage) {
2370
+ pipelineRunning = false;
2371
+ if (pipelineError)
2372
+ throw pipelineError;
2373
+ break;
1981
2374
  }
1982
- pendingAckSeq = 0;
1983
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1984
- this.clearForwardAck(ns, ackUpToSeq);
2375
+ }
2376
+ pageCount += 1;
2377
+ const ackUpToSeq = ownsCursor ? (pipelinePage?.ackUpToSeq ?? pendingAckSeq) : 0;
2378
+ const inlineAckGeneration = pipelinePage?.inlineAckGeneration ?? (ackUpToSeq > 0
2379
+ ? client._delivery.captureGroupInlineAckGeneration()
2380
+ : null);
2381
+ client._clientLog.debug(`group.v2.pull page request: group=${gid}, page=${pageCount}, after_seq=${nextAfterSeq}, limit=${limit}, ack_up_to_seq=${ackUpToSeq}, ns=${ns}`);
2382
+ let result;
2383
+ if (pipelinePage) {
2384
+ nextAfterSeq = pipelinePage.afterSeq;
2385
+ result = pipelinePage.result;
1985
2386
  }
1986
2387
  else {
1987
- client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
1988
- await this.confirmForwardGroupAck(gid, ackUpToSeq, groupAid);
1989
- pendingAckSeq = 0;
1990
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
2388
+ result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2389
+ group_id: wireGroupId,
2390
+ after_seq: nextAfterSeq,
2391
+ limit,
2392
+ ...cursorParams,
2393
+ ...(opts?.force ? { force: true } : {}),
2394
+ ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
2395
+ }, groupAid), pullGateKey);
1991
2396
  }
1992
- }
1993
- const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
1994
- if (resultGroupAid)
1995
- groupAid = resultGroupAid;
1996
- const eventGroupAid = resultGroupAid || groupAid || gid;
1997
- const pageMessages = (Array.isArray(result.messages) ? result.messages : []);
1998
- const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
1999
- const cursor = isJsonObject(result.cursor) ? result.cursor : null;
2000
- const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
2001
- const hasMore = result.has_more === true;
2002
- const serverHead = Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0);
2003
- 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 ?? '')}`);
2004
- for (const msg of messages) {
2005
- client._logMessageDebug('pull-raw', 'group.v2.pull', 'group.message_created', msg);
2006
- }
2007
- const seqs = messages
2008
- .map((msg) => Number(msg.seq ?? 0))
2009
- .filter((seq) => Number.isFinite(seq) && seq > 0);
2010
- const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
2011
- const pageTrackerSnapshot = typeof client._seqTracker.snapshotNamespace === 'function'
2012
- ? client._seqTracker.snapshotNamespace(ns)
2013
- : null;
2014
- let pageMaxSeq = nextAfterSeq;
2015
- if (seqs.length > 0) {
2016
- pageMaxSeq = Math.max(...seqs);
2017
- }
2018
- const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
2019
- const deferredKeyFetches = new Map();
2020
- let blockedSeq = 0;
2021
- const pageDecryptedBefore = decrypted.length;
2022
- for (const msg of messages) {
2023
- const seq = Number(msg.seq ?? 0);
2024
- if (!Number.isFinite(seq) || seq <= 0)
2025
- continue;
2026
- const version = String(msg.version ?? 'v2');
2027
- if (version === 'v1') {
2028
- const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
2029
- const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
2030
- const payloadObj = isJsonObject(payload) ? payload : null;
2031
- const payloadType = payloadObj ? String(payloadObj.type ?? '').trim() : '';
2032
- // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
2033
- if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
2034
- && client._delivery.recallEventFromGroupMessage(msg)) {
2035
- await client._delivery.publishGroupRecallTombstone(gid, seq, {
2036
- ...msg,
2037
- group_id: gid,
2038
- group_aid: eventGroupAid,
2039
- }, 'pull');
2040
- client._markPublishedSeq(ns, seq);
2041
- client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
2042
- continue;
2397
+ if (ackUpToSeq > 0) {
2398
+ const actualAckSeq = client._delivery.resolveGroupPullAckSeq(result, ackUpToSeq);
2399
+ if (actualAckSeq >= ackUpToSeq) {
2400
+ if (inlineAckGeneration !== null) {
2401
+ client._delivery.onGroupV2PullAckResult(gid, result, ackUpToSeq, inlineAckGeneration);
2402
+ }
2403
+ pendingAckSeq = 0;
2404
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
2405
+ this.clearForwardAck(ns, ackUpToSeq);
2406
+ }
2407
+ else {
2408
+ client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
2409
+ await this.confirmForwardGroupAck(gid, ackUpToSeq, groupAid);
2410
+ pendingAckSeq = 0;
2411
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
2043
2412
  }
2044
- if (payloadObj) {
2045
- if (!['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
2413
+ }
2414
+ const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
2415
+ if (resultGroupAid)
2416
+ groupAid = resultGroupAid;
2417
+ const eventGroupAid = resultGroupAid || groupAid || gid;
2418
+ const pageMessages = (Array.isArray(result.messages) ? result.messages : []);
2419
+ const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
2420
+ const cursor = isJsonObject(result.cursor) ? result.cursor : null;
2421
+ const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
2422
+ const hasMore = result.has_more === true;
2423
+ const serverHead = Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0);
2424
+ 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 ?? '')}`);
2425
+ for (const msg of messages) {
2426
+ client._logMessageDebug('pull-raw', 'group.v2.pull', 'group.message_created', msg);
2427
+ }
2428
+ const seqs = messages
2429
+ .map((msg) => Number(msg.seq ?? 0))
2430
+ .filter((seq) => Number.isFinite(seq) && seq > 0);
2431
+ const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
2432
+ const pageTrackerSnapshot = typeof client._seqTracker.snapshotNamespace === 'function'
2433
+ ? client._seqTracker.snapshotNamespace(ns)
2434
+ : null;
2435
+ let pageMaxSeq = nextAfterSeq;
2436
+ if (seqs.length > 0) {
2437
+ pageMaxSeq = Math.max(...seqs);
2438
+ }
2439
+ const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
2440
+ const deferredKeyFetches = new Map();
2441
+ let blockedSeq = 0;
2442
+ const pageDecryptedBefore = decrypted.length;
2443
+ for (const msg of messages) {
2444
+ const seq = Number(msg.seq ?? 0);
2445
+ if (!Number.isFinite(seq) || seq <= 0)
2446
+ continue;
2447
+ const version = String(msg.version ?? 'v2');
2448
+ if (version === 'v1') {
2449
+ const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
2450
+ const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
2451
+ const payloadObj = isJsonObject(payload) ? payload : null;
2452
+ const payloadType = payloadObj ? String(payloadObj.type ?? '').trim() : '';
2453
+ // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
2454
+ if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
2455
+ && client._delivery.recallEventFromGroupMessage(msg)) {
2456
+ await client._delivery.publishGroupRecallTombstone(gid, seq, {
2457
+ ...msg,
2458
+ group_id: gid,
2459
+ group_aid: eventGroupAid,
2460
+ }, 'pull');
2461
+ client._markPublishedSeq(ns, seq);
2462
+ client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
2463
+ continue;
2464
+ }
2465
+ if (payloadObj) {
2466
+ if (!['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
2467
+ let v1Msg = {
2468
+ message_id: String(msg.message_id ?? ''),
2469
+ from: String(msg.from_aid ?? ''),
2470
+ group_id: gid,
2471
+ group_aid: eventGroupAid,
2472
+ seq: msg.seq,
2473
+ type: String(msg.type ?? ''),
2474
+ timestamp: msg.t_server,
2475
+ payload,
2476
+ encrypted: false,
2477
+ };
2478
+ if (msg.sender_device_id !== undefined)
2479
+ v1Msg.sender_device_id = msg.sender_device_id;
2480
+ v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
2481
+ attachGatewayProximity(v1Msg, msg);
2482
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2483
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2484
+ }
2485
+ else {
2486
+ client._markPublishedSeq(ns, seq);
2487
+ }
2488
+ decrypted.push(v1Msg);
2489
+ client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
2490
+ continue;
2491
+ }
2492
+ }
2493
+ else if (payload !== undefined && payload !== null) {
2046
2494
  let v1Msg = {
2047
2495
  message_id: String(msg.message_id ?? ''),
2048
2496
  from: String(msg.from_aid ?? ''),
@@ -2054,209 +2502,208 @@ export class V2E2EECoordinator {
2054
2502
  payload,
2055
2503
  encrypted: false,
2056
2504
  };
2505
+ if (msg.sender_device_id !== undefined)
2506
+ v1Msg.sender_device_id = msg.sender_device_id;
2057
2507
  v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
2058
2508
  attachGatewayProximity(v1Msg, msg);
2059
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2060
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2061
- }
2062
- else {
2063
- client._markPublishedSeq(ns, seq);
2064
- }
2509
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2065
2510
  decrypted.push(v1Msg);
2066
2511
  client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
2067
2512
  continue;
2068
2513
  }
2514
+ if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
2515
+ const deferStatus = {};
2516
+ const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
2517
+ if (deferStatus.deferred) {
2518
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2519
+ if (deferStatus.fromAid) {
2520
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2521
+ deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
2522
+ }
2523
+ }
2524
+ if (plaintext) {
2525
+ plaintext.group_id = gid;
2526
+ plaintext.group_aid = eventGroupAid;
2527
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2528
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2529
+ }
2530
+ else {
2531
+ client._markPublishedSeq(ns, seq);
2532
+ }
2533
+ decrypted.push(plaintext);
2534
+ }
2535
+ }
2536
+ else {
2537
+ client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
2538
+ }
2539
+ continue;
2069
2540
  }
2070
- else if (payload !== undefined && payload !== null) {
2071
- let v1Msg = {
2072
- message_id: String(msg.message_id ?? ''),
2073
- from: String(msg.from_aid ?? ''),
2074
- group_id: gid,
2075
- group_aid: eventGroupAid,
2076
- seq: msg.seq,
2077
- type: String(msg.type ?? ''),
2078
- timestamp: msg.t_server,
2079
- payload,
2080
- encrypted: false,
2081
- };
2082
- v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
2083
- attachGatewayProximity(v1Msg, msg);
2084
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2085
- decrypted.push(v1Msg);
2086
- client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
2541
+ if (version !== 'v2') {
2542
+ client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
2087
2543
  continue;
2088
2544
  }
2089
- if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
2090
- const deferStatus = {};
2091
- const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
2092
- if (deferStatus.deferred) {
2093
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2094
- if (deferStatus.fromAid) {
2095
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2096
- deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
2545
+ const deferStatus = {};
2546
+ let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
2547
+ if (deferStatus.deferred) {
2548
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2549
+ }
2550
+ if (deferStatus.deferred && deferStatus.fromAid) {
2551
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2552
+ deferredKeyFetches.set(key, {
2553
+ fromAid: deferStatus.fromAid,
2554
+ senderDeviceId: deferStatus.senderDeviceId ?? '',
2555
+ groupId: deferStatus.groupId ?? '',
2556
+ });
2557
+ }
2558
+ if (plaintext === null) {
2559
+ client._clientLog.debug(`group.v2.pull decrypt returned null: group=${gid}, seq=${seq}`);
2560
+ continue;
2561
+ }
2562
+ plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
2563
+ plaintext.group_id = gid;
2564
+ plaintext.group_aid = eventGroupAid;
2565
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2566
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2567
+ }
2568
+ else {
2569
+ client._markPublishedSeq(ns, seq);
2570
+ }
2571
+ decrypted.push(plaintext);
2572
+ client._logMessageDebug('decrypt-ok', 'group.v2.pull', 'group.message_created', plaintext);
2573
+ }
2574
+ const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
2575
+ const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
2576
+ const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
2577
+ const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq'));
2578
+ const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
2579
+ const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
2580
+ const commitTarget = Math.max(pageContigBefore, effectiveFloor, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, ownsCursor ? cursorCurrentSeq : 0, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
2581
+ let ackSeq = pageContigBefore;
2582
+ let contigAdvanced = false;
2583
+ if (ownsCursor) {
2584
+ try {
2585
+ if (commitTarget > pageContigBefore) {
2586
+ client._seqTracker.forceContiguousSeq(ns, commitTarget);
2587
+ if (effectiveFloor > pageContigBefore) {
2588
+ client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
2097
2589
  }
2098
- }
2099
- if (plaintext) {
2100
- plaintext.group_id = gid;
2101
- plaintext.group_aid = eventGroupAid;
2102
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2103
- await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2590
+ if (deferredServerCursor > pageContigBefore && pageCount === 1) {
2591
+ client._clientLog.info(`group.v2.pull restore local A from deferred server ACK cursor: group=${gid}, ns=${ns}, contiguous=${pageContigBefore} -> current_seq=${deferredServerCursor}`);
2104
2592
  }
2105
- else {
2106
- client._markPublishedSeq(ns, seq);
2593
+ if (seqs.length > 0) {
2594
+ client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
2107
2595
  }
2108
- decrypted.push(plaintext);
2596
+ }
2597
+ ackSeq = client._seqTracker.getContiguousSeq(ns);
2598
+ contigAdvanced = ackSeq !== pageContigBefore;
2599
+ if (contigAdvanced) {
2600
+ await client._commitSeqTrackerState(ns);
2601
+ if (pageCount === 1 && deferredServerCursor > 0)
2602
+ this.clearForwardCursor(ns, ackSeq);
2109
2603
  }
2110
2604
  }
2111
- else {
2112
- client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
2605
+ catch (exc) {
2606
+ if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
2607
+ client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
2608
+ }
2609
+ client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
2610
+ throw exc;
2113
2611
  }
2114
- continue;
2115
- }
2116
- if (version !== 'v2') {
2117
- client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
2118
- continue;
2119
- }
2120
- const deferStatus = {};
2121
- let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
2122
- if (deferStatus.deferred) {
2123
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2124
- }
2125
- if (deferStatus.deferred && deferStatus.fromAid) {
2126
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2127
- deferredKeyFetches.set(key, {
2128
- fromAid: deferStatus.fromAid,
2129
- senderDeviceId: deferStatus.senderDeviceId ?? '',
2130
- groupId: deferStatus.groupId ?? '',
2131
- });
2612
+ if (contigAdvanced)
2613
+ await client._drainOrderedMessages(ns, undefined, false, false, 'pull');
2132
2614
  }
2133
- if (plaintext === null) {
2134
- client._clientLog.debug(`group.v2.pull decrypt returned null: group=${gid}, seq=${seq}`);
2135
- continue;
2136
- }
2137
- plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
2138
- plaintext.group_id = gid;
2139
- plaintext.group_aid = eventGroupAid;
2140
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2141
- await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2615
+ for (const fetch of deferredKeyFetches.values()) {
2616
+ this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
2142
2617
  }
2143
- else {
2144
- client._markPublishedSeq(ns, seq);
2618
+ if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
2619
+ this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
2145
2620
  }
2146
- decrypted.push(plaintext);
2147
- client._logMessageDebug('decrypt-ok', 'group.v2.pull', 'group.message_created', plaintext);
2148
- }
2149
- const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
2150
- const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
2151
- const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
2152
- const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq'));
2153
- const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
2154
- const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
2155
- const commitTarget = Math.max(pageContigBefore, effectiveFloor, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, ownsCursor ? cursorCurrentSeq : 0, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
2156
- let ackSeq = pageContigBefore;
2157
- let contigAdvanced = false;
2158
- if (ownsCursor) {
2159
- try {
2160
- if (commitTarget > pageContigBefore) {
2161
- client._seqTracker.forceContiguousSeq(ns, commitTarget);
2162
- if (effectiveFloor > pageContigBefore) {
2163
- client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
2164
- }
2165
- if (deferredServerCursor > pageContigBefore && pageCount === 1) {
2166
- client._clientLog.info(`group.v2.pull restore local A from deferred server ACK cursor: group=${gid}, ns=${ns}, contiguous=${pageContigBefore} -> current_seq=${deferredServerCursor}`);
2167
- }
2168
- if (seqs.length > 0) {
2169
- client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
2170
- }
2171
- }
2172
- ackSeq = client._seqTracker.getContiguousSeq(ns);
2173
- contigAdvanced = ackSeq !== pageContigBefore;
2174
- if (contigAdvanced) {
2175
- await client._commitSeqTrackerState(ns);
2176
- if (pageCount === 1 && deferredServerCursor > 0)
2177
- this.clearForwardCursor(ns, ackSeq);
2621
+ // 无本页 cursor.current_seq 时,只有本页成功提交了比已知 cursor
2622
+ // 更高的 A 才新增 ACK 义务;空页不得因已有本地 A 重复确认。
2623
+ const knownServerCursorSeq = hasServerCursor
2624
+ ? cursorCurrentSeq
2625
+ : (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
2626
+ const ackNeeded = ackSeq > 0
2627
+ && ackSeq > lastAutoAckSeq
2628
+ && ownsCursor
2629
+ && ((hasServerCursor && ackSeq > cursorCurrentSeq)
2630
+ || (contigAdvanced && ackSeq > knownServerCursorSeq));
2631
+ if (ackNeeded) {
2632
+ this.recordForwardAck(ns, ackSeq);
2633
+ const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
2634
+ const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
2635
+ if (canContinuePage && !pipelineRunning) {
2636
+ pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
2637
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
2638
+ client._clientLog.debug(`group.v2.pull queued piggyback auto-ack: group=${gid}, ns=${ns}, ack_seq=${pendingAckSeq}, raw_count=${messages.length}`);
2178
2639
  }
2179
- }
2180
- catch (exc) {
2181
- if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
2182
- client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
2640
+ else {
2641
+ client._clientLog.debug(`group.v2.pull sending terminal auto-ack: group=${gid}, ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
2642
+ await this.confirmForwardGroupAck(gid, ackSeq, groupAid);
2643
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
2183
2644
  }
2184
- client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
2185
- throw exc;
2186
2645
  }
2187
- if (contigAdvanced)
2188
- await client._drainOrderedMessages(ns, undefined, false, false, 'pull');
2189
- }
2190
- for (const fetch of deferredKeyFetches.values()) {
2191
- this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
2192
- }
2193
- if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
2194
- this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
2195
- }
2196
- // 无本页 cursor.current_seq 时,只有本页成功提交了比已知 cursor
2197
- // 更高的 A 才新增 ACK 义务;空页不得因已有本地 A 重复确认。
2198
- const knownServerCursorSeq = hasServerCursor
2199
- ? cursorCurrentSeq
2200
- : (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
2201
- const ackNeeded = ackSeq > 0
2202
- && ackSeq > lastAutoAckSeq
2203
- && ownsCursor
2204
- && ((hasServerCursor && ackSeq > cursorCurrentSeq)
2205
- || (contigAdvanced && ackSeq > knownServerCursorSeq));
2206
- if (ackNeeded) {
2207
- this.recordForwardAck(ns, ackSeq);
2646
+ client._delivery.syncProgress(ns, {
2647
+ rawCount: messages.length,
2648
+ pulledCount: decrypted.length - pageDecryptedBefore,
2649
+ remaining: result.remaining,
2650
+ hasMore: hasMoreField ? hasMore : undefined,
2651
+ currentSeq: client._seqTracker.getContiguousSeq(ns),
2652
+ targetSeq: serverHead,
2653
+ });
2208
2654
  const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
2209
- const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
2210
- if (canContinuePage) {
2211
- pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
2212
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
2213
- client._clientLog.debug(`group.v2.pull queued piggyback auto-ack: group=${gid}, ns=${ns}, ack_seq=${pendingAckSeq}, raw_count=${messages.length}`);
2655
+ if (!ownsCursor)
2656
+ break;
2657
+ const rawCount = messages.length;
2658
+ if (blockedSeq > 0)
2659
+ break;
2660
+ const shouldContinue = shouldContinueForwardPage({
2661
+ rawCount,
2662
+ nextAfterSeq,
2663
+ nextAfter,
2664
+ limit,
2665
+ hasMoreField,
2666
+ hasMore,
2667
+ serverHead,
2668
+ observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._delivery.pendingPullUpper?.(ns) ?? 0)),
2669
+ pendingAckSeq,
2670
+ });
2671
+ const shouldContinueForPipeline = shouldContinueForwardPage({
2672
+ rawCount,
2673
+ nextAfterSeq,
2674
+ nextAfter,
2675
+ limit,
2676
+ hasMoreField,
2677
+ hasMore,
2678
+ serverHead,
2679
+ observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._delivery.pendingPullUpper?.(ns) ?? 0)),
2680
+ pendingAckSeq: 0,
2681
+ });
2682
+ if (!shouldContinue)
2683
+ break;
2684
+ if (shouldContinueForPipeline && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
2685
+ if (pendingAckSeq > 0) {
2686
+ await this.confirmForwardGroupAck(gid, pendingAckSeq, groupAid);
2687
+ pendingAckSeq = 0;
2688
+ }
2689
+ startPipeline(nextAfter);
2214
2690
  }
2215
- else {
2216
- client._clientLog.debug(`group.v2.pull sending terminal auto-ack: group=${gid}, ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
2217
- await this.confirmForwardGroupAck(gid, ackSeq, groupAid);
2218
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
2691
+ if (!pipelineRunning && pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
2692
+ const nextKey = pullGateKeyForClient(client, 'group.v2.pull', {
2693
+ group_id: gid, after_seq: nextAfter, force: opts?.force === true, limit,
2694
+ _group_cursor_params: cursorParams,
2695
+ }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
2696
+ await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
2697
+ pullGateKey = nextKey;
2219
2698
  }
2699
+ const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
2700
+ if (tailDelayMs > 0)
2701
+ await v2Sleep(tailDelayMs);
2702
+ nextAfterSeq = nextAfter;
2220
2703
  }
2221
- client._delivery.syncProgress(ns, {
2222
- rawCount: messages.length,
2223
- pulledCount: decrypted.length - pageDecryptedBefore,
2224
- remaining: result.remaining,
2225
- hasMore: hasMoreField ? hasMore : undefined,
2226
- currentSeq: client._seqTracker.getContiguousSeq(ns),
2227
- targetSeq: serverHead,
2228
- });
2229
- const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
2230
- if (!ownsCursor)
2231
- break;
2232
- const rawCount = messages.length;
2233
- if (blockedSeq > 0)
2234
- break;
2235
- const shouldContinue = shouldContinueForwardPage({
2236
- rawCount,
2237
- nextAfterSeq,
2238
- nextAfter,
2239
- limit,
2240
- hasMoreField,
2241
- hasMore,
2242
- serverHead,
2243
- observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._delivery.pendingPullUpper?.(ns) ?? 0)),
2244
- pendingAckSeq,
2245
- });
2246
- if (!shouldContinue)
2247
- break;
2248
- if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
2249
- const nextKey = pullGateKeyForClient(client, 'group.v2.pull', {
2250
- group_id: gid, after_seq: nextAfter, force: opts?.force === true, limit,
2251
- _group_cursor_params: cursorParams,
2252
- }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
2253
- await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
2254
- pullGateKey = nextKey;
2255
- }
2256
- const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
2257
- if (tailDelayMs > 0)
2258
- await v2Sleep(tailDelayMs);
2259
- nextAfterSeq = nextAfter;
2704
+ }
2705
+ finally {
2706
+ await stopPipeline();
2260
2707
  }
2261
2708
  if (pageCount >= maxPages) {
2262
2709
  client._clientLog.warn(`group.v2.pull reached max_pages=${maxPages} group=${gid} after_seq=${nextAfterSeq}`);
@@ -2751,7 +3198,6 @@ export class V2E2EECoordinator {
2751
3198
  seq: msg.seq,
2752
3199
  timestamp: (msg.timestamp ?? msg.t_server),
2753
3200
  device_id: msg.device_id,
2754
- slot_id: msg.slot_id,
2755
3201
  _decrypt_error: 'encrypted push payload is not decryptable on raw push path',
2756
3202
  _decrypt_stage: 'push_envelope',
2757
3203
  };
@@ -2795,13 +3241,18 @@ export class V2E2EECoordinator {
2795
3241
  encrypted: true,
2796
3242
  e2ee,
2797
3243
  };
2798
- result.direction = fromAid && fromAid === client._aid ? 'outbound_sync' : 'inbound';
3244
+ const senderDeviceId = String(aad.from_device ?? '').trim();
3245
+ if (senderDeviceId)
3246
+ result.sender_device_id = senderDeviceId;
3247
+ const direction = String(msg.direction ?? '').trim();
3248
+ if (direction)
3249
+ result.direction = direction;
3250
+ else if (fromAid && fromAid !== client._aid)
3251
+ result.direction = 'inbound';
2799
3252
  if (msg.t_server !== undefined)
2800
3253
  result.t_server = msg.t_server;
2801
3254
  if (msg.device_id !== undefined)
2802
3255
  result.device_id = msg.device_id;
2803
- if (msg.slot_id !== undefined)
2804
- result.slot_id = msg.slot_id;
2805
3256
  attachGatewayProximity(result, msg);
2806
3257
  if (group) {
2807
3258
  result.group_id = (msg.group_id ?? aad.group_id ?? envelope.group_id);
@@ -2839,10 +3290,10 @@ export class V2E2EECoordinator {
2839
3290
  }
2840
3291
  return await this.publishEncryptedPushAsUndecryptable(undecryptableEvent, ns, seq, msg, group);
2841
3292
  }
2842
- async decryptV2PushMessage(data) {
3293
+ async decryptV2PushMessage(data, deferStatus) {
2843
3294
  if (!isJsonObject(data))
2844
3295
  return null;
2845
- return await this.client._decryptV2Message(data, false, false, false, false, undefined, false, 'inline_push');
3296
+ return await this.client._decryptV2Message(data, false, true, false, false, deferStatus, false, 'inline_push');
2846
3297
  }
2847
3298
  handleGroupChangedSpk(data, groupId, action) {
2848
3299
  if (!this.client._v2Session || !groupId || !MEMBERSHIP_ACTIONS.has(action))