@agentunion/fastaun-browser 0.5.6 → 0.5.8

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 (102) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/_packed_docs/CHANGELOG.md +74 -0
  3. package/_packed_docs/INDEX.md +12 -11
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +4 -3
  5. package/_packed_docs/agent.md/SCHEMA.md +83 -58
  6. package/_packed_docs/agent.md/examples/codeagent-claudecode.md +1 -1
  7. package/_packed_docs/agent.md/examples/openclaw-lobster.md +1 -1
  8. package/_packed_docs/agent.md/examples/signed-openclaw-lobster.md +1 -1
  9. package/_packed_docs/audit/AUN/346/234/215/345/212/241Go/345/214/226/351/207/215/346/236/204/347/262/276/347/273/206/345/214/226/346/272/220/347/240/201/345/256/241/346/237/245-20260718.md +3 -3
  10. package/_packed_docs/aun/345/205/254/347/275/221/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +7 -0
  11. 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 +422 -331
  12. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +598 -398
  13. package/_packed_docs/group-message-rpc-alignment-gaps.md +741 -0
  14. package/_packed_docs/message-online-push-alignment.md +572 -0
  15. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1 -1
  16. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +10 -10
  17. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +2 -2
  18. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +30 -10
  19. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +23 -20
  20. package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +3 -3
  21. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +5 -6
  22. package/_packed_docs/sdk/09-group-rpc-manual.md +11 -7
  23. package/_packed_docs/sdk/09-message-rpc-manual.md +5 -3
  24. package/_packed_docs/sdk/09-storage-rpc-manual.md +14 -3
  25. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +19 -17
  26. package/_packed_docs/sdk/CHANGELOG-0.5.6.md +73 -0
  27. package/_packed_docs/sdk/INDEX.md +22 -22
  28. package/_packed_docs/sdk/README.md +6 -5
  29. package/_packed_docs//345/217/221/345/270/203/346/212/245/345/221/212-0.5.6.md +260 -0
  30. package/dist/agent-md-schema.d.ts +14 -0
  31. package/dist/agent-md-schema.d.ts.map +1 -0
  32. package/dist/agent-md-schema.js +103 -0
  33. package/dist/agent-md-schema.js.map +1 -0
  34. package/dist/agent-md.d.ts +7 -0
  35. package/dist/agent-md.d.ts.map +1 -1
  36. package/dist/agent-md.js +229 -10
  37. package/dist/agent-md.js.map +1 -1
  38. package/dist/aid-store.d.ts +5 -1
  39. package/dist/aid-store.d.ts.map +1 -1
  40. package/dist/aid-store.js +26 -4
  41. package/dist/aid-store.js.map +1 -1
  42. package/dist/aid.d.ts +2 -0
  43. package/dist/aid.d.ts.map +1 -1
  44. package/dist/aid.js +29 -7
  45. package/dist/aid.js.map +1 -1
  46. package/dist/auth.d.ts.map +1 -1
  47. package/dist/auth.js +1 -0
  48. package/dist/auth.js.map +1 -1
  49. package/dist/bundle.js +29895 -21968
  50. package/dist/cert-utils.d.ts +1 -0
  51. package/dist/cert-utils.d.ts.map +1 -1
  52. package/dist/cert-utils.js +36 -16
  53. package/dist/cert-utils.js.map +1 -1
  54. package/dist/client/delivery.d.ts +33 -5
  55. package/dist/client/delivery.d.ts.map +1 -1
  56. package/dist/client/delivery.js +597 -220
  57. package/dist/client/delivery.js.map +1 -1
  58. package/dist/client/lifecycle.d.ts +15 -1
  59. package/dist/client/lifecycle.d.ts.map +1 -1
  60. package/dist/client/lifecycle.js +452 -151
  61. package/dist/client/lifecycle.js.map +1 -1
  62. package/dist/client/rpc-pipeline.d.ts +17 -6
  63. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  64. package/dist/client/rpc-pipeline.js +284 -115
  65. package/dist/client/rpc-pipeline.js.map +1 -1
  66. package/dist/client/runtime.d.ts +1 -3
  67. package/dist/client/runtime.d.ts.map +1 -1
  68. package/dist/client/runtime.js +4 -7
  69. package/dist/client/runtime.js.map +1 -1
  70. package/dist/client/v2-e2ee.d.ts +20 -2
  71. package/dist/client/v2-e2ee.d.ts.map +1 -1
  72. package/dist/client/v2-e2ee.js +398 -114
  73. package/dist/client/v2-e2ee.js.map +1 -1
  74. package/dist/client.d.ts +22 -10
  75. package/dist/client.d.ts.map +1 -1
  76. package/dist/client.js +396 -104
  77. package/dist/client.js.map +1 -1
  78. package/dist/facades.d.ts +23 -1
  79. package/dist/facades.d.ts.map +1 -1
  80. package/dist/facades.js +51 -7
  81. package/dist/facades.js.map +1 -1
  82. package/dist/group-id.d.ts.map +1 -1
  83. package/dist/group-id.js +2 -17
  84. package/dist/group-id.js.map +1 -1
  85. package/dist/index.d.ts +4 -3
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +2 -1
  88. package/dist/index.js.map +1 -1
  89. package/dist/seq-tracker.d.ts.map +1 -1
  90. package/dist/seq-tracker.js +8 -2
  91. package/dist/seq-tracker.js.map +1 -1
  92. package/dist/tools/cross-sdk-agent.js +23 -1
  93. package/dist/tools/cross-sdk-agent.js.map +1 -1
  94. package/dist/transport.d.ts.map +1 -1
  95. package/dist/transport.js +8 -1
  96. package/dist/transport.js.map +1 -1
  97. package/dist/validators.d.ts.map +1 -1
  98. package/dist/validators.js +8 -14
  99. package/dist/validators.js.map +1 -1
  100. package/dist/version.d.ts +1 -1
  101. package/dist/version.js +1 -1
  102. package/package.json +6 -5
@@ -21,6 +21,9 @@ function strictWindowSeq(value, field, positive = false) {
21
21
  throw new ValidationError(`${field} must be >= ${minimum}`);
22
22
  return value;
23
23
  }
24
+ function safeNonNegativeSeq(value) {
25
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
26
+ }
24
27
  function strictWindowLimit(value) {
25
28
  const limit = strictWindowSeq(value, 'limit', true);
26
29
  if (limit > V2_PULL_MAX_PAGE_LIMIT)
@@ -53,26 +56,50 @@ function deduplicateForwardPageMessages(value, afterSeq) {
53
56
  if (!isJsonObject(raw))
54
57
  continue;
55
58
  const row = raw;
56
- const seq = Number(row.seq ?? 0);
57
- if (!Number.isSafeInteger(seq) || seq <= afterSeq || bySeq.has(seq))
59
+ const seq = safeNonNegativeSeq(row.seq);
60
+ if (seq === null || seq <= 0 || seq <= afterSeq || bySeq.has(seq))
58
61
  continue;
59
62
  bySeq.set(seq, row);
60
63
  }
61
64
  return [...bySeq.entries()].sort(([a], [b]) => a - b).map(([, row]) => row);
62
65
  }
63
- function validateTailPage(result, pageLimit) {
66
+ function shouldContinueForwardPage({ rawCount, nextAfterSeq, nextAfter, limit, hasMoreField, hasMore, serverHead, pendingAckSeq, }) {
67
+ // 无新 seq 或游标未前进时必须停止,避免错误元数据造成循环。
68
+ if (rawCount <= 0 || nextAfter <= nextAfterSeq)
69
+ return false;
70
+ if (hasMore || serverHead > nextAfter || pendingAckSeq > 0)
71
+ return true;
72
+ // 仅旧服务缺失 has_more 时,以满页保留兼容续拉。
73
+ return !hasMoreField && limit > 0 && rawCount >= limit;
74
+ }
75
+ function validateTailPage(result, afterSeq, pageLimit) {
64
76
  if (!isJsonObject(result))
65
77
  throw new ValidationError('tail response must be an object');
66
78
  const row = result;
67
79
  if (row.window_mode !== 'tail')
68
80
  throw new ValidationError('tail response window_mode must equal tail');
81
+ strictWindowSeq(afterSeq, 'after_seq');
69
82
  const windowStart = strictWindowSeq(row.window_start_seq, 'window_start_seq');
70
83
  const head = strictWindowSeq(row.head_seq, 'head_seq');
71
84
  const covered = strictWindowSeq(row.covered_through_seq, 'covered_through_seq');
72
85
  if (windowStart > head || covered !== head || head - windowStart + 1 > pageLimit) {
73
86
  throw new ValidationError('tail response proof is invalid');
74
87
  }
75
- return { windowStart, head, messages: deduplicateWindowMessages(row.messages, windowStart, head, 'the proven tail window') };
88
+ let lowerSeq = windowStart;
89
+ if (head <= afterSeq) {
90
+ if (windowStart !== head)
91
+ throw new ValidationError('empty tail window must end at head_seq');
92
+ lowerSeq = head + 1;
93
+ }
94
+ else {
95
+ if (windowStart < afterSeq + 1)
96
+ throw new ValidationError('tail window_start_seq is below after_seq');
97
+ lowerSeq = Math.max(windowStart, afterSeq + 1);
98
+ }
99
+ const messages = deduplicateWindowMessages(row.messages, lowerSeq, head, 'the proven tail window');
100
+ if (head <= afterSeq && messages.length > 0)
101
+ throw new ValidationError('tail response must not include messages at or below after_seq');
102
+ return { windowStart, head, messages };
76
103
  }
77
104
  function validateHistoryPage(result, beforeSeq) {
78
105
  if (!isJsonObject(result))
@@ -329,6 +356,9 @@ export class V2E2EECoordinator {
329
356
  runtime;
330
357
  targetSetCache = new Map();
331
358
  senderIKSyncFetches = new Map();
359
+ /** 服务端 ACK cursor 高于本地 A 时,延迟到下一次 Forward Pull 再恢复。 */
360
+ pendingForwardCursors = new Map();
361
+ pendingForwardAcks = new Map();
332
362
  v2SessionInitRetryTimer = null;
333
363
  v2SessionInitRetryBackgroundSync = false;
334
364
  v2SessionInitRetryIdentityKey = null;
@@ -338,6 +368,51 @@ export class V2E2EECoordinator {
338
368
  get client() {
339
369
  return this.runtime.client;
340
370
  }
371
+ resetForwardCursorState() {
372
+ this.pendingForwardCursors.clear();
373
+ this.pendingForwardAcks.clear();
374
+ }
375
+ pendingForwardCursor(ns, localAck) {
376
+ if (!ns)
377
+ return 0;
378
+ const pending = this.pendingForwardCursors.get(ns) ?? 0;
379
+ if (pending <= localAck) {
380
+ if (pending > 0)
381
+ this.pendingForwardCursors.delete(ns);
382
+ return 0;
383
+ }
384
+ return pending;
385
+ }
386
+ recordForwardCursor(ns, cursor, localAck) {
387
+ if (!ns || !Number.isSafeInteger(cursor) || cursor <= localAck)
388
+ return;
389
+ this.pendingForwardCursors.set(ns, Math.max(this.pendingForwardCursors.get(ns) ?? 0, cursor));
390
+ }
391
+ clearForwardCursor(ns, committedAck) {
392
+ const pending = this.pendingForwardCursors.get(ns) ?? 0;
393
+ if (pending > 0 && committedAck >= pending)
394
+ this.pendingForwardCursors.delete(ns);
395
+ }
396
+ pendingForwardAck(ns, localAck) {
397
+ if (!ns)
398
+ return 0;
399
+ const pending = this.pendingForwardAcks.get(ns) ?? 0;
400
+ if (!Number.isSafeInteger(pending) || pending <= 0 || localAck <= 0 || pending > localAck) {
401
+ this.pendingForwardAcks.delete(ns);
402
+ return 0;
403
+ }
404
+ return pending;
405
+ }
406
+ recordForwardAck(ns, ackSeq) {
407
+ if (!ns || !Number.isSafeInteger(ackSeq) || ackSeq <= 0)
408
+ return;
409
+ this.pendingForwardAcks.set(ns, Math.max(this.pendingForwardAcks.get(ns) ?? 0, ackSeq));
410
+ }
411
+ clearForwardAck(ns, confirmedSeq) {
412
+ const pending = this.pendingForwardAcks.get(ns) ?? 0;
413
+ if (pending > 0 && confirmedSeq >= pending)
414
+ this.pendingForwardAcks.delete(ns);
415
+ }
341
416
  get bootstrapCache() {
342
417
  const client = this.client;
343
418
  if (!(client._v2BootstrapCache instanceof Map)) {
@@ -759,7 +834,8 @@ export class V2E2EECoordinator {
759
834
  createdAt: Date.now(),
760
835
  });
761
836
  client._clientLog.debug(`V2 decrypt pending sender IK: key=${messageKey} from=${fromAid} device=${senderDeviceId || '-'} group=${groupId || '<p2p>'} pending=${client._v2SenderIKPending.size}`);
762
- this.scheduleSenderIKFetch(fromAid, senderDeviceId, groupId);
837
+ if (args.startFetch !== false)
838
+ this.scheduleSenderIKFetch(fromAid, senderDeviceId, groupId);
763
839
  }
764
840
  scheduleSenderIKFetch(fromAid, senderDeviceId, groupId) {
765
841
  const client = this.client;
@@ -810,8 +886,9 @@ export class V2E2EECoordinator {
810
886
  const pendingItems = [...client._v2SenderIKPending.entries()].filter(([, entry]) => entry.fromAid === fromAid && entry.senderDeviceId === senderDeviceId && entry.groupId === groupId);
811
887
  for (const [key, entry] of pendingItems) {
812
888
  let plaintext = null;
889
+ const retryStatus = {};
813
890
  try {
814
- plaintext = await client._decryptV2Message(entry.msg, false);
891
+ plaintext = await client._decryptV2Message(entry.msg, false, true, true, true, retryStatus);
815
892
  }
816
893
  catch (exc) {
817
894
  client._clientLog.warn(`V2 sender IK pending retry raised: key=${key} err=${String(formatE2EEError(exc))}`);
@@ -819,6 +896,10 @@ export class V2E2EECoordinator {
819
896
  continue;
820
897
  }
821
898
  if (plaintext === null) {
899
+ if (retryStatus.deferred) {
900
+ client._clientLog.warn(`V2 pending retry still missing key material: key=${key}`);
901
+ continue;
902
+ }
822
903
  client._clientLog.warn(`V2 sender IK pending retry still undecryptable: key=${key}`);
823
904
  const isGroup = Boolean(entry.groupId);
824
905
  const eventName = isGroup ? 'group.message_undecryptable' : 'message.undecryptable';
@@ -907,7 +988,7 @@ export class V2E2EECoordinator {
907
988
  history_error: { code: 'decrypt_failed', message: reason },
908
989
  };
909
990
  }
910
- async decodeP2PWindowMessage(msg, publish, history, source) {
991
+ async decodeP2PWindowMessage(msg, publish, history, source, orderedPublish = false) {
911
992
  const client = this.client;
912
993
  const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
913
994
  let plaintext = null;
@@ -939,16 +1020,27 @@ export class V2E2EECoordinator {
939
1020
  }
940
1021
  if (publish) {
941
1022
  const event = client._delivery.p2pAppEventForMessage(plaintext);
942
- await client._publishAppEvent(event.event, event.payload, source);
1023
+ if (orderedPublish) {
1024
+ await client._delivery.publishOrderedMessage(event.event, client._aid ? `p2p:${client._aid}` : '', seq, event.payload);
1025
+ }
1026
+ else {
1027
+ await client._publishAppEvent(event.event, event.payload, source);
1028
+ }
943
1029
  }
944
1030
  return plaintext;
945
1031
  }
946
- async decodeGroupWindowMessage(msg, groupId, groupAid, publish, history, source) {
1032
+ async decodeGroupWindowMessage(msg, groupId, groupAid, publish, history, source, orderedPublish = false) {
947
1033
  const client = this.client;
948
1034
  const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
949
1035
  let plaintext = null;
950
1036
  if (String(msg.version ?? '') === 'v1') {
951
1037
  const payload = msg.payload;
1038
+ if (payload === undefined || payload === null) {
1039
+ if (history)
1040
+ return this.historyDecryptFailure(msg, 'legacy payload is missing');
1041
+ client._clientLog.warn(`Group Tail 缺少 payload 的 V1 行已跳过,连续性证明仍保留: group=${groupId}, seq=${seq}`);
1042
+ return null;
1043
+ }
952
1044
  if (isJsonObject(payload)
953
1045
  && ['e2ee.encrypted', 'e2ee.group_encrypted'].includes(String(payload.type ?? ''))) {
954
1046
  if (history)
@@ -985,8 +1077,14 @@ export class V2E2EECoordinator {
985
1077
  client._clientLog.warn(`Group Tail 单条消息解密失败,连续性证明仍保留: group=${groupId}, seq=${seq}`);
986
1078
  return null;
987
1079
  }
988
- if (publish)
989
- await client._publishAppEvent('group.message_created', plaintext, source);
1080
+ if (publish) {
1081
+ if (orderedPublish) {
1082
+ await client._delivery.publishOrderedMessage('group.message_created', `group:${groupId}`, seq, plaintext);
1083
+ }
1084
+ else {
1085
+ await client._publishAppEvent('group.message_created', plaintext, source);
1086
+ }
1087
+ }
990
1088
  return plaintext;
991
1089
  }
992
1090
  async decodeInlineP2PMessage(msg) {
@@ -1003,7 +1101,7 @@ export class V2E2EECoordinator {
1003
1101
  client._persistSeq(ns);
1004
1102
  await client._saveSeqTrackerState?.();
1005
1103
  }
1006
- async pullV2TailInternal(params) {
1104
+ async pullV2TailInternal(params, deferPublish = false) {
1007
1105
  const client = this.client;
1008
1106
  if (params.window_mode !== 'tail')
1009
1107
  throw new ValidationError('window_mode must equal tail');
@@ -1016,7 +1114,7 @@ export class V2E2EECoordinator {
1016
1114
  const floor = Number(result.retention_floor_seq ?? 0);
1017
1115
  const previous = ns ? client._seqTracker.getSyncState(ns) : null;
1018
1116
  await this.applyTailFloor(ns, Number.isSafeInteger(floor) ? floor : 0);
1019
- const page = validateTailPage(result, limit);
1117
+ const page = validateTailPage(result, afterSeq, limit);
1020
1118
  if (ns) {
1021
1119
  client._seqTracker.commitTailWindow(ns, {
1022
1120
  windowStartSeq: page.windowStart, headSeq: page.head, coveredThroughSeq: page.head, pageLimit: limit,
@@ -1026,7 +1124,7 @@ export class V2E2EECoordinator {
1026
1124
  }
1027
1125
  const decoded = [];
1028
1126
  for (const msg of page.messages) {
1029
- const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail');
1127
+ const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail', deferPublish);
1030
1128
  if (plaintext)
1031
1129
  decoded.push(plaintext);
1032
1130
  }
@@ -1053,14 +1151,33 @@ export class V2E2EECoordinator {
1053
1151
  await client._ensureV2SessionReady('message.pull');
1054
1152
  const ns = client._aid ? `p2p:${client._aid}` : '';
1055
1153
  if (opts?.windowMode === 'tail') {
1154
+ if (!opts.gateLocked) {
1155
+ const key = `${ns}|tail|after=${afterSeq}|limit=${limit}`;
1156
+ return await client._runPullSerialized(key, () => this.pullV2(afterSeq, limit, {
1157
+ ...(opts ?? {}),
1158
+ gateLocked: true,
1159
+ }), false);
1160
+ }
1056
1161
  const result = await this.pullV2TailInternal({ window_mode: 'tail', after_seq: afterSeq, limit });
1057
1162
  return Array.isArray(result.messages) ? result.messages : [];
1058
1163
  }
1164
+ if (ns && !opts?.gateLocked) {
1165
+ const key = `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`;
1166
+ return await client._runPullSerialized(key, () => this.pullV2(afterSeq, limit, {
1167
+ ...(opts ?? {}),
1168
+ gateLocked: true,
1169
+ }), true);
1170
+ }
1059
1171
  const decrypted = [];
1060
1172
  let nextAfterSeq = opts?.force ? afterSeq : (afterSeq || (ns ? client._seqTracker.getContiguousSeq(ns) : 0));
1173
+ let pullGateKey = ns
1174
+ ? `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`
1175
+ : '';
1176
+ const deferredServerCursor = ns ? this.pendingForwardCursor(ns, client._seqTracker.getContiguousSeq(ns)) : 0;
1177
+ const deferredForwardAck = ns ? this.pendingForwardAck(ns, client._seqTracker.getContiguousSeq(ns)) : 0;
1061
1178
  let pageCount = 0;
1062
1179
  let lastAutoAckSeq = 0;
1063
- let pendingAckSeq = 0;
1180
+ let pendingAckSeq = deferredForwardAck;
1064
1181
  const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
1065
1182
  while (pageCount < maxPages) {
1066
1183
  pageCount += 1;
@@ -1075,24 +1192,29 @@ export class V2E2EECoordinator {
1075
1192
  ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
1076
1193
  });
1077
1194
  if (ackUpToSeq > 0) {
1078
- if (pendingInlineGeneration !== null) {
1079
- let actualAckSeq = ackUpToSeq;
1080
- if (Object.prototype.hasOwnProperty.call(result, 'server_ack_seq')) {
1081
- actualAckSeq = typeof result.server_ack_seq === 'number'
1082
- && Number.isSafeInteger(result.server_ack_seq) && result.server_ack_seq >= 0
1083
- ? result.server_ack_seq
1084
- : 0;
1085
- }
1086
- if (actualAckSeq > 0) {
1195
+ const actualAckSeq = client._delivery?.resolveP2PPullAckSeq?.(result, ackUpToSeq) ?? 0;
1196
+ if (actualAckSeq >= ackUpToSeq) {
1197
+ if (pendingInlineGeneration !== null) {
1087
1198
  client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
1088
1199
  }
1200
+ pendingAckSeq = 0;
1201
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1202
+ this.clearForwardAck(ns, ackUpToSeq);
1203
+ }
1204
+ else {
1205
+ client._clientLog.warn(`message.v2.pull piggyback ACK below target: requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
1206
+ await this.confirmForwardP2PAck(ackUpToSeq, pendingInlineGeneration ?? undefined);
1207
+ pendingAckSeq = 0;
1208
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1089
1209
  }
1090
- pendingAckSeq = 0;
1091
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1092
1210
  }
1093
1211
  const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1094
1212
  const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
1095
- 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_ack_seq=${String(result.server_ack_seq ?? '')}`);
1213
+ const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1214
+ const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
1215
+ const hasMore = result.has_more === true;
1216
+ const serverHead = safeNonNegativeSeq(cursor?.latest_seq) ?? 0;
1217
+ 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 ?? '')}`);
1096
1218
  const seqs = messages
1097
1219
  .map((msg) => Number(msg.seq ?? 0))
1098
1220
  .filter((seq) => Number.isFinite(seq) && seq > 0);
@@ -1101,6 +1223,7 @@ export class V2E2EECoordinator {
1101
1223
  ? client._seqTracker.snapshotNamespace(ns)
1102
1224
  : null;
1103
1225
  const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
1226
+ const deferredKeyFetches = new Map();
1104
1227
  for (const msg of messages) {
1105
1228
  const seq = Number(msg.seq ?? 0);
1106
1229
  if (!Number.isFinite(seq) || seq <= 0)
@@ -1127,7 +1250,7 @@ export class V2E2EECoordinator {
1127
1250
  attachGatewayProximity(v1Msg, msg);
1128
1251
  const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
1129
1252
  if (ns)
1130
- await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload);
1253
+ await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
1131
1254
  else
1132
1255
  await client._publishAppEvent(appEvent.event, appEvent.payload);
1133
1256
  decrypted.push(v1Msg);
@@ -1145,11 +1268,20 @@ export class V2E2EECoordinator {
1145
1268
  if (msgSpkId && client._v2Session && !client._v2Session.isCurrentSPK(msgSpkId)) {
1146
1269
  client._v2Session.trackOldSPKMaxSeq(msgSpkId, seq);
1147
1270
  }
1148
- const plaintext = await client._decryptV2Message(msg);
1271
+ const deferStatus = {};
1272
+ const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true);
1273
+ if (deferStatus.deferred && deferStatus.fromAid) {
1274
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1275
+ deferredKeyFetches.set(key, {
1276
+ fromAid: deferStatus.fromAid,
1277
+ senderDeviceId: deferStatus.senderDeviceId ?? '',
1278
+ groupId: deferStatus.groupId ?? '',
1279
+ });
1280
+ }
1149
1281
  if (plaintext === null)
1150
1282
  continue;
1151
1283
  if (ns) {
1152
- await client._publishPulledMessage('message.received', ns, seq, plaintext);
1284
+ await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1153
1285
  decrypted.push(plaintext);
1154
1286
  }
1155
1287
  else {
@@ -1158,10 +1290,11 @@ export class V2E2EECoordinator {
1158
1290
  }
1159
1291
  }
1160
1292
  const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
1161
- const serverAckSeq = Number(result.server_ack_seq ?? 0);
1293
+ const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
1294
+ const serverAckSeq = parsedServerAckSeq ?? 0;
1162
1295
  const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
1163
1296
  if (ns) {
1164
- const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
1297
+ const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
1165
1298
  let ackSeq = pageContigBefore;
1166
1299
  let contigAdvanced = false;
1167
1300
  try {
@@ -1170,6 +1303,9 @@ export class V2E2EECoordinator {
1170
1303
  if (retentionFloor > pageContigBefore) {
1171
1304
  client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${pageContigBefore} -> retention_floor_seq=${retentionFloor}`);
1172
1305
  }
1306
+ if (deferredServerCursor > pageContigBefore && pageCount === 1) {
1307
+ client._clientLog.info(`message.v2.pull restore local A from deferred server ACK cursor: ns=${ns}, contiguous=${pageContigBefore} -> server_ack_seq=${deferredServerCursor}`);
1308
+ }
1173
1309
  if (seqs.length > 0) {
1174
1310
  client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
1175
1311
  }
@@ -1177,10 +1313,9 @@ export class V2E2EECoordinator {
1177
1313
  ackSeq = client._seqTracker.getContiguousSeq(ns);
1178
1314
  contigAdvanced = ackSeq !== pageContigBefore;
1179
1315
  if (contigAdvanced) {
1180
- await client._drainOrderedMessages(ns);
1181
- ackSeq = client._seqTracker.getContiguousSeq(ns);
1182
- contigAdvanced = ackSeq !== pageContigBefore;
1183
1316
  await client._commitSeqTrackerState(ns);
1317
+ if (pageCount === 1 && deferredServerCursor > 0)
1318
+ this.clearForwardCursor(ns, ackSeq);
1184
1319
  }
1185
1320
  }
1186
1321
  catch (exc) {
@@ -1190,11 +1325,25 @@ export class V2E2EECoordinator {
1190
1325
  client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
1191
1326
  throw exc;
1192
1327
  }
1193
- const ackNeeded = pageMessages.length > 0
1194
- && ackSeq > 0
1328
+ if (contigAdvanced)
1329
+ await client._drainOrderedMessages(ns, undefined, false, false);
1330
+ for (const fetch of deferredKeyFetches.values()) {
1331
+ this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1332
+ }
1333
+ if (parsedServerAckSeq !== null && serverAckSeq > ackSeq) {
1334
+ this.recordForwardCursor(ns, serverAckSeq, ackSeq);
1335
+ }
1336
+ // 无本页服务端水位时,只有本页成功提交了比已知服务端更高的 A
1337
+ // 才新增 ACK 义务。空页不能仅因本地已有 A 而重复 ACK。
1338
+ const knownServerAckSeq = hasServerAckSeq
1339
+ ? serverAckSeq
1340
+ : (pageCount === 1 ? deferredServerCursor : 0);
1341
+ const ackNeeded = ackSeq > 0
1195
1342
  && ackSeq > lastAutoAckSeq
1196
- && (contigAdvanced || (hasServerAckSeq && ackSeq > serverAckSeq));
1343
+ && ((hasServerAckSeq && ackSeq > serverAckSeq)
1344
+ || (contigAdvanced && ackSeq > knownServerAckSeq));
1197
1345
  if (ackNeeded) {
1346
+ this.recordForwardAck(ns, ackSeq);
1198
1347
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
1199
1348
  const canContinuePage = messages.length > 0 && nextAfter > nextAfterSeq;
1200
1349
  if (canContinuePage) {
@@ -1202,18 +1351,30 @@ export class V2E2EECoordinator {
1202
1351
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1203
1352
  }
1204
1353
  else {
1205
- await this.ackV2(ackSeq);
1354
+ await this.confirmForwardP2PAck(ackSeq);
1206
1355
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1207
1356
  }
1208
1357
  }
1209
1358
  }
1210
1359
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
1211
1360
  const rawCount = messages.length;
1212
- const canContinuePage = rawCount > 0 && nextAfter > nextAfterSeq;
1213
- const fullPage = canContinuePage && limit > 0 && rawCount >= limit;
1214
- const shouldContinue = canContinuePage && (fullPage || pendingAckSeq > 0);
1361
+ const shouldContinue = shouldContinueForwardPage({
1362
+ rawCount,
1363
+ nextAfterSeq,
1364
+ nextAfter,
1365
+ limit,
1366
+ hasMoreField,
1367
+ hasMore,
1368
+ serverHead,
1369
+ pendingAckSeq,
1370
+ });
1215
1371
  if (!shouldContinue)
1216
1372
  break;
1373
+ if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
1374
+ const nextKey = `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`;
1375
+ await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
1376
+ pullGateKey = nextKey;
1377
+ }
1217
1378
  const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
1218
1379
  if (tailDelayMs > 0)
1219
1380
  await v2Sleep(tailDelayMs);
@@ -1222,13 +1383,34 @@ export class V2E2EECoordinator {
1222
1383
  if (pageCount >= maxPages) {
1223
1384
  client._clientLog.warn(`message.v2.pull reached max_pages=${maxPages} after_seq=${nextAfterSeq}`);
1224
1385
  if (pendingAckSeq > 0) {
1225
- await this.ackV2(pendingAckSeq);
1386
+ await this.confirmForwardP2PAck(pendingAckSeq);
1226
1387
  pendingAckSeq = 0;
1227
1388
  }
1228
1389
  }
1229
1390
  return decrypted;
1230
1391
  }
1392
+ async confirmForwardP2PAck(upToSeq, inlineGeneration) {
1393
+ const ns = this.client._aid ? `p2p:${this.client._aid}` : '';
1394
+ try {
1395
+ const result = await this.ackV2ForForward(upToSeq, inlineGeneration);
1396
+ const actualAckSeq = this.client._delivery?.resolveForwardAckSeq?.(result, upToSeq) ?? 0;
1397
+ if (actualAckSeq < upToSeq) {
1398
+ throw new E2EEError(`message.v2.ack server ACK watermark ${actualAckSeq} is below requested ${upToSeq}`);
1399
+ }
1400
+ this.clearForwardAck(ns, upToSeq);
1401
+ }
1402
+ catch (exc) {
1403
+ this.recordForwardAck(ns, upToSeq);
1404
+ throw exc;
1405
+ }
1406
+ }
1231
1407
  async ackV2(upToSeq, inlineGeneration) {
1408
+ return this.ackV2WithConfirmation(upToSeq, inlineGeneration, false);
1409
+ }
1410
+ async ackV2ForForward(upToSeq, inlineGeneration) {
1411
+ return this.ackV2WithConfirmation(upToSeq, inlineGeneration, true);
1412
+ }
1413
+ async ackV2WithConfirmation(upToSeq, inlineGeneration, requireServerWatermark) {
1232
1414
  const client = this.client;
1233
1415
  const ns = client._aid ? `p2p:${client._aid}` : '';
1234
1416
  let seq = upToSeq ?? (ns ? client._seqTracker.getContiguousSeq(ns) : 0);
@@ -1237,23 +1419,33 @@ export class V2E2EECoordinator {
1237
1419
  seq = client._clampAckSeq('message.v2.ack', 'up_to_seq', ns, seq);
1238
1420
  if (!Number.isSafeInteger(seq) || seq <= 0)
1239
1421
  return { acked: 0 };
1422
+ // ACK 发出前绑定当时的 inline pending;迟到响应不得结算重置后新建的 pending。
1423
+ const ackGeneration = inlineGeneration ?? client._delivery?.pendingP2PInlineAckGeneration?.(ns) ?? null;
1424
+ const v2SessionAtStart = client._v2Session;
1425
+ const v2RuntimeGenerationAtStart = Number(client._v2RuntimeGeneration ?? 0);
1240
1426
  const raw = await client._callRawV2Rpc('message.v2.ack', { up_to_seq: seq });
1241
1427
  const result = isJsonObject(raw)
1242
1428
  ? { ...raw }
1243
1429
  : { result: raw };
1244
- const actualAckSeq = client._delivery?.resolveActualAckSeq?.(result, seq) ?? seq;
1430
+ const actualAckSeq = requireServerWatermark
1431
+ ? (client._delivery?.resolveForwardAckSeq?.(result, seq) ?? 0)
1432
+ : (client._delivery?.resolveActualAckSeq?.(result, seq) ?? seq);
1433
+ if (requireServerWatermark && actualAckSeq < seq) {
1434
+ throw new E2EEError(`message.v2.ack server ACK watermark ${actualAckSeq} is below requested ${seq}`);
1435
+ }
1245
1436
  result.ack_seq = actualAckSeq;
1246
1437
  result.success = true;
1247
- if (inlineGeneration === undefined) {
1248
- client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq);
1249
- }
1250
- else {
1251
- client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, inlineGeneration);
1438
+ if (ackGeneration !== null) {
1439
+ client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, ackGeneration);
1252
1440
  }
1253
- if (client._v2Session) {
1441
+ if (v2SessionAtStart
1442
+ && client._v2Session === v2SessionAtStart
1443
+ && Number(client._v2RuntimeGeneration ?? 0) === v2RuntimeGenerationAtStart) {
1254
1444
  try {
1255
- const destroyed = await client._v2Session.maybeDestroyOldSPKs(actualAckSeq);
1256
- if (destroyed.length > 0) {
1445
+ const destroyed = await v2SessionAtStart.maybeDestroyOldSPKs(actualAckSeq);
1446
+ if (client._v2Session === v2SessionAtStart
1447
+ && Number(client._v2RuntimeGeneration ?? 0) === v2RuntimeGenerationAtStart
1448
+ && destroyed.length > 0) {
1257
1449
  client._clientLog.info(`V2 destroyed old SPKs after ack: ${destroyed.slice(0, 3)} (PFS)`);
1258
1450
  }
1259
1451
  }
@@ -1334,7 +1526,7 @@ export class V2E2EECoordinator {
1334
1526
  throw exc;
1335
1527
  }
1336
1528
  }
1337
- async pullGroupV2TailInternal(params) {
1529
+ async pullGroupV2TailInternal(params, deferPublish = false) {
1338
1530
  const client = this.client;
1339
1531
  if (params.window_mode !== 'tail')
1340
1532
  throw new ValidationError('window_mode must equal tail');
@@ -1356,7 +1548,7 @@ export class V2E2EECoordinator {
1356
1548
  const floor = Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0);
1357
1549
  const previous = client._seqTracker.getSyncState(ns);
1358
1550
  await this.applyTailFloor(ns, Number.isSafeInteger(floor) ? floor : 0);
1359
- const page = validateTailPage(result, limit);
1551
+ const page = validateTailPage(result, afterSeq, limit);
1360
1552
  client._seqTracker.commitTailWindow(ns, {
1361
1553
  windowStartSeq: page.windowStart, headSeq: page.head, coveredThroughSeq: page.head, pageLimit: limit,
1362
1554
  });
@@ -1364,7 +1556,8 @@ export class V2E2EECoordinator {
1364
1556
  await client._saveSeqTrackerState?.();
1365
1557
  const decoded = [];
1366
1558
  for (const msg of page.messages) {
1367
- const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, true, false, 'tail');
1559
+ client._logMessageDebug?.('tail-raw', 'group.v2.pull', 'group.message_created', msg);
1560
+ const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, true, false, 'tail', deferPublish);
1368
1561
  if (plaintext)
1369
1562
  decoded.push(plaintext);
1370
1563
  }
@@ -1411,22 +1604,43 @@ export class V2E2EECoordinator {
1411
1604
  let groupAid = String(opts?.groupAid ?? '').trim();
1412
1605
  const ns = `group:${gid}`;
1413
1606
  if (opts?.windowMode === 'tail') {
1607
+ if (!opts.gateLocked) {
1608
+ const key = `${ns}|tail|after=${afterSeq}|limit=${limit}`;
1609
+ return await client._runPullSerialized(key, () => this.pullGroupV2(gid, afterSeq, limit, {
1610
+ ...(opts ?? {}),
1611
+ gateLocked: true,
1612
+ }), false);
1613
+ }
1414
1614
  const result = await this.pullGroupV2TailInternal({
1415
1615
  group_id: String(opts.wireGroupId ?? gid), group_aid: groupAid || undefined,
1416
1616
  window_mode: 'tail', after_seq: afterSeq, limit,
1417
1617
  });
1418
1618
  return Array.isArray(result.messages) ? result.messages : [];
1419
1619
  }
1620
+ if (!opts?.gateLocked) {
1621
+ const key = `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`;
1622
+ return await client._runPullSerialized(key, () => this.pullGroupV2(gid, afterSeq, limit, {
1623
+ ...(opts ?? {}),
1624
+ gateLocked: true,
1625
+ }), true);
1626
+ }
1420
1627
  const decrypted = [];
1421
1628
  const wireGroupId = String(opts?.wireGroupId ?? groupId ?? '').trim() || gid;
1422
1629
  const cursorParams = opts?.cursorParams ?? {};
1423
1630
  const ownsCursor = opts?.ownsCursor !== false;
1631
+ let pullGateKey = ownsCursor
1632
+ ? `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`
1633
+ : '';
1424
1634
  let nextAfterSeq = (opts?.explicitAfterSeq || opts?.force)
1425
1635
  ? afterSeq
1426
1636
  : (afterSeq || client._seqTracker.getContiguousSeq(ns));
1637
+ const deferredServerCursor = ownsCursor
1638
+ ? this.pendingForwardCursor(ns, client._seqTracker.getContiguousSeq(ns))
1639
+ : 0;
1640
+ const deferredForwardAck = ownsCursor ? this.pendingForwardAck(ns, client._seqTracker.getContiguousSeq(ns)) : 0;
1427
1641
  let pageCount = 0;
1428
1642
  let lastAutoAckSeq = 0;
1429
- let pendingAckSeq = 0;
1643
+ let pendingAckSeq = deferredForwardAck;
1430
1644
  const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
1431
1645
  while (pageCount < maxPages) {
1432
1646
  pageCount += 1;
@@ -1443,23 +1657,21 @@ export class V2E2EECoordinator {
1443
1657
  ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
1444
1658
  }, groupAid));
1445
1659
  if (ackUpToSeq > 0) {
1446
- if (pendingInlineGeneration !== null) {
1447
- let actualAckSeq = ackUpToSeq;
1448
- const ackCursor = isJsonObject(result.cursor)
1449
- ? result.cursor
1450
- : null;
1451
- if (ackCursor && Object.prototype.hasOwnProperty.call(ackCursor, 'current_seq')) {
1452
- actualAckSeq = typeof ackCursor.current_seq === 'number'
1453
- && Number.isSafeInteger(ackCursor.current_seq) && ackCursor.current_seq >= 0
1454
- ? ackCursor.current_seq
1455
- : 0;
1456
- }
1457
- if (actualAckSeq > 0) {
1660
+ const actualAckSeq = client._delivery?.resolveGroupPullAckSeq?.(result, ackUpToSeq) ?? 0;
1661
+ if (actualAckSeq >= ackUpToSeq) {
1662
+ if (pendingInlineGeneration !== null) {
1458
1663
  client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
1459
1664
  }
1665
+ pendingAckSeq = 0;
1666
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1667
+ this.clearForwardAck(ns, ackUpToSeq);
1668
+ }
1669
+ else {
1670
+ client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
1671
+ await this.confirmForwardGroupAck(gid, ackUpToSeq, groupAid);
1672
+ pendingAckSeq = 0;
1673
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1460
1674
  }
1461
- pendingAckSeq = 0;
1462
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1463
1675
  }
1464
1676
  const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
1465
1677
  if (resultGroupAid)
@@ -1467,6 +1679,10 @@ export class V2E2EECoordinator {
1467
1679
  const eventGroupAid = resultGroupAid || groupAid || gid;
1468
1680
  const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1469
1681
  const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
1682
+ const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1683
+ const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
1684
+ const hasMore = result.has_more === true;
1685
+ const serverHead = Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0);
1470
1686
  const seqs = messages
1471
1687
  .map((msg) => Number(msg.seq ?? 0))
1472
1688
  .filter((seq) => Number.isFinite(seq) && seq > 0);
@@ -1475,6 +1691,7 @@ export class V2E2EECoordinator {
1475
1691
  ? client._seqTracker.snapshotNamespace(ns)
1476
1692
  : null;
1477
1693
  const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
1694
+ const deferredKeyFetches = new Map();
1478
1695
  for (const msg of messages) {
1479
1696
  const seq = Number(msg.seq ?? 0);
1480
1697
  if (!Number.isFinite(seq) || seq <= 0)
@@ -1510,7 +1727,7 @@ export class V2E2EECoordinator {
1510
1727
  };
1511
1728
  v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
1512
1729
  attachGatewayProximity(v1Msg, msg);
1513
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg);
1730
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
1514
1731
  decrypted.push(v1Msg);
1515
1732
  continue;
1516
1733
  }
@@ -1529,7 +1746,7 @@ export class V2E2EECoordinator {
1529
1746
  };
1530
1747
  v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
1531
1748
  attachGatewayProximity(v1Msg, msg);
1532
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg);
1749
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
1533
1750
  decrypted.push(v1Msg);
1534
1751
  continue;
1535
1752
  }
@@ -1540,56 +1757,83 @@ export class V2E2EECoordinator {
1540
1757
  client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
1541
1758
  continue;
1542
1759
  }
1543
- let plaintext = await client._decryptV2Message(msg);
1760
+ const deferStatus = {};
1761
+ let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true);
1762
+ if (deferStatus.deferred && deferStatus.fromAid) {
1763
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1764
+ deferredKeyFetches.set(key, {
1765
+ fromAid: deferStatus.fromAid,
1766
+ senderDeviceId: deferStatus.senderDeviceId ?? '',
1767
+ groupId: deferStatus.groupId ?? '',
1768
+ });
1769
+ }
1544
1770
  if (plaintext === null)
1545
1771
  continue;
1546
1772
  plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
1547
1773
  plaintext.group_id = gid;
1548
1774
  plaintext.group_aid = eventGroupAid;
1549
- await client._publishPulledMessage('group.message_created', ns, seq, plaintext);
1775
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
1550
1776
  decrypted.push(plaintext);
1551
1777
  }
1552
- const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1553
- const cursorCurrentSeq = Number(cursor?.current_seq ?? 0);
1778
+ const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
1779
+ const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
1780
+ const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
1554
1781
  const retentionFloor = Math.max(Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0), Number(cursor?.retention_floor_seq ?? 0));
1555
1782
  const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
1556
1783
  const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
1557
- const commitTarget = Math.max(pageContigBefore, Number.isFinite(effectiveFloor) ? effectiveFloor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
1784
+ const commitTarget = Math.max(pageContigBefore, Number.isFinite(effectiveFloor) ? effectiveFloor : 0, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
1558
1785
  let ackSeq = pageContigBefore;
1559
1786
  let contigAdvanced = false;
1560
- try {
1561
- if (commitTarget > pageContigBefore) {
1562
- client._seqTracker.forceContiguousSeq(ns, commitTarget);
1563
- if (effectiveFloor > pageContigBefore) {
1564
- client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
1565
- }
1566
- if (seqs.length > 0) {
1567
- client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
1787
+ if (ownsCursor) {
1788
+ try {
1789
+ if (commitTarget > pageContigBefore) {
1790
+ client._seqTracker.forceContiguousSeq(ns, commitTarget);
1791
+ if (effectiveFloor > pageContigBefore) {
1792
+ client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
1793
+ }
1794
+ if (deferredServerCursor > pageContigBefore && pageCount === 1) {
1795
+ client._clientLog.info(`group.v2.pull restore local A from deferred server ACK cursor: group=${gid}, ns=${ns}, contiguous=${pageContigBefore} -> current_seq=${deferredServerCursor}`);
1796
+ }
1797
+ if (seqs.length > 0) {
1798
+ client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
1799
+ }
1568
1800
  }
1569
- }
1570
- ackSeq = client._seqTracker.getContiguousSeq(ns);
1571
- contigAdvanced = ackSeq !== pageContigBefore;
1572
- if (contigAdvanced) {
1573
- await client._drainOrderedMessages(ns);
1574
1801
  ackSeq = client._seqTracker.getContiguousSeq(ns);
1575
1802
  contigAdvanced = ackSeq !== pageContigBefore;
1576
- await client._commitSeqTrackerState(ns);
1803
+ if (contigAdvanced) {
1804
+ await client._commitSeqTrackerState(ns);
1805
+ if (pageCount === 1 && deferredServerCursor > 0)
1806
+ this.clearForwardCursor(ns, ackSeq);
1807
+ }
1577
1808
  }
1578
- }
1579
- catch (exc) {
1580
- if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
1581
- client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
1809
+ catch (exc) {
1810
+ if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
1811
+ client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
1812
+ }
1813
+ client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
1814
+ throw exc;
1582
1815
  }
1583
- client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
1584
- throw exc;
1816
+ if (contigAdvanced)
1817
+ await client._drainOrderedMessages(ns, undefined, false, false);
1585
1818
  }
1586
- const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
1587
- const ackNeeded = pageMessages.length > 0
1588
- && ackSeq > 0
1819
+ for (const fetch of deferredKeyFetches.values()) {
1820
+ this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1821
+ }
1822
+ if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
1823
+ this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
1824
+ }
1825
+ // 无本页 cursor.current_seq 时,只有本页成功提交了比已知 cursor
1826
+ // 更高的 A 才新增 ACK 义务;空页不得因已有本地 A 重复确认。
1827
+ const knownServerCursorSeq = hasServerCursor
1828
+ ? cursorCurrentSeq
1829
+ : (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
1830
+ const ackNeeded = ackSeq > 0
1589
1831
  && ackSeq > lastAutoAckSeq
1590
1832
  && ownsCursor
1591
- && (contigAdvanced || (hasServerCursor && ackSeq > cursorCurrentSeq));
1833
+ && ((hasServerCursor && ackSeq > cursorCurrentSeq)
1834
+ || (contigAdvanced && ackSeq > knownServerCursorSeq));
1592
1835
  if (ackNeeded) {
1836
+ this.recordForwardAck(ns, ackSeq);
1593
1837
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
1594
1838
  const canContinuePage = messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
1595
1839
  if (canContinuePage) {
@@ -1597,7 +1841,7 @@ export class V2E2EECoordinator {
1597
1841
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1598
1842
  }
1599
1843
  else {
1600
- await this.ackGroupV2(gid, ackSeq);
1844
+ await this.confirmForwardGroupAck(gid, ackSeq, groupAid);
1601
1845
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1602
1846
  }
1603
1847
  }
@@ -1605,11 +1849,23 @@ export class V2E2EECoordinator {
1605
1849
  if (!ownsCursor)
1606
1850
  break;
1607
1851
  const rawCount = messages.length;
1608
- const canContinuePage = rawCount > 0 && nextAfter > nextAfterSeq;
1609
- const fullPage = canContinuePage && limit > 0 && rawCount >= limit;
1610
- const shouldContinue = canContinuePage && (fullPage || pendingAckSeq > 0);
1852
+ const shouldContinue = shouldContinueForwardPage({
1853
+ rawCount,
1854
+ nextAfterSeq,
1855
+ nextAfter,
1856
+ limit,
1857
+ hasMoreField,
1858
+ hasMore,
1859
+ serverHead,
1860
+ pendingAckSeq,
1861
+ });
1611
1862
  if (!shouldContinue)
1612
1863
  break;
1864
+ if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
1865
+ const nextKey = `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`;
1866
+ await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
1867
+ pullGateKey = nextKey;
1868
+ }
1613
1869
  const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
1614
1870
  if (tailDelayMs > 0)
1615
1871
  await v2Sleep(tailDelayMs);
@@ -1618,13 +1874,34 @@ export class V2E2EECoordinator {
1618
1874
  if (pageCount >= maxPages) {
1619
1875
  client._clientLog.warn(`group.v2.pull reached max_pages=${maxPages} group=${gid} after_seq=${nextAfterSeq}`);
1620
1876
  if (pendingAckSeq > 0) {
1621
- await this.ackGroupV2(gid, pendingAckSeq);
1877
+ await this.confirmForwardGroupAck(gid, pendingAckSeq, groupAid);
1622
1878
  pendingAckSeq = 0;
1623
1879
  }
1624
1880
  }
1625
1881
  return decrypted;
1626
1882
  }
1883
+ async confirmForwardGroupAck(groupId, upToSeq, groupAid) {
1884
+ const ns = `group:${String(groupId ?? '').trim()}`;
1885
+ try {
1886
+ const result = await this.ackGroupV2ForForward(groupId, upToSeq, groupAid);
1887
+ const actualAckSeq = this.client._delivery?.resolveForwardAckSeq?.(result, upToSeq) ?? 0;
1888
+ if (actualAckSeq < upToSeq) {
1889
+ throw new E2EEError(`group.v2.ack server ACK watermark ${actualAckSeq} is below requested ${upToSeq}`);
1890
+ }
1891
+ this.clearForwardAck(ns, upToSeq);
1892
+ }
1893
+ catch (exc) {
1894
+ this.recordForwardAck(ns, upToSeq);
1895
+ throw exc;
1896
+ }
1897
+ }
1627
1898
  async ackGroupV2(groupId, upToSeq, groupAid, inlineGeneration) {
1899
+ return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, inlineGeneration, false);
1900
+ }
1901
+ async ackGroupV2ForForward(groupId, upToSeq, groupAid) {
1902
+ return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, undefined, true);
1903
+ }
1904
+ async ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, inlineGeneration, requireServerWatermark) {
1628
1905
  const client = this.client;
1629
1906
  const gid = typeof groupId === 'string' ? groupId.trim() : '';
1630
1907
  if (!gid)
@@ -1644,13 +1921,17 @@ export class V2E2EECoordinator {
1644
1921
  seq = client._clampAckSeq('group.v2.ack', 'up_to_seq', ns, seq);
1645
1922
  if (!Number.isSafeInteger(seq) || seq <= 0)
1646
1923
  return { acked: 0 };
1924
+ // 与 P2P 一致,必须将 ACK 响应限定到请求开始时对应的 inline generation。
1925
+ const ackGeneration = inlineGeneration ?? client._delivery?.pendingGroupInlineAckGeneration?.(ns) ?? null;
1647
1926
  const raw = await client._callRawV2Rpc('group.v2.ack', withExplicitGroupAid({ group_id: gid, up_to_seq: seq }, explicitGroupAid));
1648
- const actualAckSeq = client._delivery?.resolveActualAckSeq?.(raw, seq) ?? seq;
1649
- if (inlineGeneration === undefined) {
1650
- client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq);
1927
+ const actualAckSeq = requireServerWatermark
1928
+ ? (client._delivery?.resolveForwardAckSeq?.(raw, seq) ?? 0)
1929
+ : (client._delivery?.resolveActualAckSeq?.(raw, seq) ?? seq);
1930
+ if (requireServerWatermark && actualAckSeq < seq) {
1931
+ throw new E2EEError(`group.v2.ack server ACK watermark ${actualAckSeq} is below requested ${seq}`);
1651
1932
  }
1652
- else {
1653
- client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq, inlineGeneration);
1933
+ if (ackGeneration !== null) {
1934
+ client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq, ackGeneration);
1654
1935
  }
1655
1936
  return raw;
1656
1937
  }
@@ -1830,7 +2111,7 @@ export class V2E2EECoordinator {
1830
2111
  let e2ee;
1831
2112
  if (payload?.type === 'e2ee.group_encrypted' && String(payload.version ?? '') === 'v2') {
1832
2113
  e2ee = v2E2eeMeta(payload);
1833
- const plain = await client._decryptV2EnvelopeForThought({ envelope: payload, fromAid });
2114
+ const plain = await client._decryptV2EnvelopeForThought({ envelope: payload, fromAid, fetchSenderIK: true });
1834
2115
  if (plain === null) {
1835
2116
  decryptFailed = true;
1836
2117
  client._clientLog.debug(`group.thought.get decrypt returned null: group=${groupId}, thought_id=${thoughtId}, from=${fromAid}`);
@@ -1900,7 +2181,7 @@ export class V2E2EECoordinator {
1900
2181
  let e2ee;
1901
2182
  if (payload?.type === 'e2ee.p2p_encrypted' && String(payload.version ?? '') === 'v2') {
1902
2183
  e2ee = v2E2eeMeta(payload);
1903
- const plain = await client._decryptV2EnvelopeForThought({ envelope: payload, fromAid });
2184
+ const plain = await client._decryptV2EnvelopeForThought({ envelope: payload, fromAid, fetchSenderIK: true });
1904
2185
  if (plain === null) {
1905
2186
  decryptFailed = true;
1906
2187
  client._clientLog.debug(`message.thought.get decrypt returned null: thought_id=${thoughtId}, from=${fromAid}, to=${toAid}`);
@@ -1987,7 +2268,10 @@ export class V2E2EECoordinator {
1987
2268
  const fromAid = String(opts.fromAid || aad.from || '').trim();
1988
2269
  const senderDeviceId = String(aad.from_device ?? '');
1989
2270
  const senderCertFingerprint = String(envelope.sender_cert_fingerprint ?? '').trim().toLowerCase();
1990
- const senderPubDer = await this.getV2SenderPubDer(fromAid, senderDeviceId, senderCertFingerprint);
2271
+ let senderPubDer = await this.getV2SenderPubDer(fromAid, senderDeviceId, senderCertFingerprint);
2272
+ if (!senderPubDer && opts.fetchSenderIK) {
2273
+ senderPubDer = await this.fetchSenderIKOnce(fromAid, senderDeviceId, groupIdForKeys, senderCertFingerprint);
2274
+ }
1991
2275
  if (!senderPubDer) {
1992
2276
  client._clientLog.warn(`V2 thought decrypt: no sender IK for ${fromAid} device=${senderDeviceId}`);
1993
2277
  this.scheduleSenderIKFetch(fromAid, senderDeviceId, groupIdForKeys);