@agentunion/fastaun 0.5.6 → 0.5.9

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 (92) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/_packed_docs/CHANGELOG.md +106 -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/agent.md/examples//347/276/244/347/273/204-/345/274/200/345/217/221/345/233/242/351/230/237.md +21 -0
  10. 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
  11. 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
  12. 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
  13. 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
  14. package/_packed_docs/group-message-rpc-alignment-gaps.md +741 -0
  15. package/_packed_docs/message-online-push-alignment.md +572 -0
  16. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1 -1
  17. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +10 -10
  18. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +2 -2
  19. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +30 -10
  20. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +23 -20
  21. package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +3 -3
  22. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +5 -6
  23. package/_packed_docs/sdk/09-group-rpc-manual.md +11 -7
  24. package/_packed_docs/sdk/09-message-rpc-manual.md +5 -3
  25. package/_packed_docs/sdk/09-storage-rpc-manual.md +14 -3
  26. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +19 -17
  27. package/_packed_docs/sdk/CHANGELOG-0.5.6.md +73 -0
  28. package/_packed_docs/sdk/INDEX.md +22 -22
  29. package/_packed_docs/sdk/README.md +6 -5
  30. package/dist/agent-md-schema.d.ts +16 -0
  31. package/dist/agent-md-schema.js +109 -0
  32. package/dist/agent-md-schema.js.map +1 -0
  33. package/dist/agent-md.d.ts +7 -0
  34. package/dist/agent-md.js +91 -9
  35. package/dist/agent-md.js.map +1 -1
  36. package/dist/aid-store.d.ts +5 -1
  37. package/dist/aid-store.js +25 -6
  38. package/dist/aid-store.js.map +1 -1
  39. package/dist/aid.d.ts +2 -0
  40. package/dist/aid.js +32 -12
  41. package/dist/aid.js.map +1 -1
  42. package/dist/auth.d.ts +3 -1
  43. package/dist/auth.js +65 -13
  44. package/dist/auth.js.map +1 -1
  45. package/dist/cert-utils.d.ts +2 -1
  46. package/dist/cert-utils.js +32 -16
  47. package/dist/cert-utils.js.map +1 -1
  48. package/dist/client/delivery.d.ts +36 -7
  49. package/dist/client/delivery.js +701 -216
  50. package/dist/client/delivery.js.map +1 -1
  51. package/dist/client/group-state.js +2 -2
  52. package/dist/client/group-state.js.map +1 -1
  53. package/dist/client/lifecycle.d.ts +15 -1
  54. package/dist/client/lifecycle.js +475 -152
  55. package/dist/client/lifecycle.js.map +1 -1
  56. package/dist/client/rpc-pipeline.d.ts +24 -6
  57. package/dist/client/rpc-pipeline.js +372 -131
  58. package/dist/client/rpc-pipeline.js.map +1 -1
  59. package/dist/client/runtime.d.ts +1 -3
  60. package/dist/client/runtime.js +4 -7
  61. package/dist/client/runtime.js.map +1 -1
  62. package/dist/client/v2-e2ee.d.ts +20 -3
  63. package/dist/client/v2-e2ee.js +376 -93
  64. package/dist/client/v2-e2ee.js.map +1 -1
  65. package/dist/client.d.ts +23 -10
  66. package/dist/client.js +515 -150
  67. package/dist/client.js.map +1 -1
  68. package/dist/discovery.d.ts +1 -0
  69. package/dist/discovery.js +3 -0
  70. package/dist/discovery.js.map +1 -1
  71. package/dist/facades.d.ts +23 -1
  72. package/dist/facades.js +59 -7
  73. package/dist/facades.js.map +1 -1
  74. package/dist/group-id.js +2 -17
  75. package/dist/group-id.js.map +1 -1
  76. package/dist/index.d.ts +4 -3
  77. package/dist/index.js +2 -1
  78. package/dist/index.js.map +1 -1
  79. package/dist/net.d.ts +3 -0
  80. package/dist/net.js +13 -0
  81. package/dist/net.js.map +1 -1
  82. package/dist/seq-tracker.js +6 -1
  83. package/dist/seq-tracker.js.map +1 -1
  84. package/dist/tools/cross-sdk-agent.js +29 -3
  85. package/dist/tools/cross-sdk-agent.js.map +1 -1
  86. package/dist/transport.js +8 -1
  87. package/dist/transport.js.map +1 -1
  88. package/dist/validators.js +8 -14
  89. package/dist/validators.js.map +1 -1
  90. package/dist/version.d.ts +1 -1
  91. package/dist/version.js +1 -1
  92. package/package.json +4 -3
@@ -18,6 +18,9 @@ function strictWindowSeq(value, field, positive = false) {
18
18
  throw new ValidationError(`${field} must be >= ${minimum}`);
19
19
  return value;
20
20
  }
21
+ function safeNonNegativeSeq(value) {
22
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
23
+ }
21
24
  function strictWindowLimit(value) {
22
25
  const limit = strictWindowSeq(value, 'limit', true);
23
26
  if (limit > V2_PULL_MAX_PAGE_LIMIT)
@@ -50,19 +53,29 @@ function deduplicateForwardPageMessages(value, afterSeq) {
50
53
  if (!isJsonObject(raw))
51
54
  continue;
52
55
  const row = raw;
53
- const seq = Number(row.seq ?? 0);
54
- if (!Number.isSafeInteger(seq) || seq <= afterSeq || bySeq.has(seq))
56
+ const seq = safeNonNegativeSeq(row.seq);
57
+ if (seq === null || seq <= 0 || seq <= afterSeq || bySeq.has(seq))
55
58
  continue;
56
59
  bySeq.set(seq, row);
57
60
  }
58
61
  return [...bySeq.entries()].sort(([a], [b]) => a - b).map(([, row]) => row);
59
62
  }
60
- function validateTailPage(result, pageLimit) {
63
+ function shouldContinueForwardPage({ rawCount, nextAfterSeq, nextAfter, limit, hasMoreField, hasMore, serverHead, pendingAckSeq, }) {
64
+ // 无新 seq 或游标未前进时必须停止,避免错误元数据造成循环。
65
+ if (rawCount <= 0 || nextAfter <= nextAfterSeq)
66
+ return false;
67
+ if (hasMore || serverHead > nextAfter || pendingAckSeq > 0)
68
+ return true;
69
+ // 仅旧服务缺失 has_more 时,以满页保留兼容续拉。
70
+ return !hasMoreField && limit > 0 && rawCount >= limit;
71
+ }
72
+ function validateTailPage(result, afterSeq, pageLimit) {
61
73
  if (!isJsonObject(result))
62
74
  throw new ValidationError('tail response must be an object');
63
75
  const row = result;
64
76
  if (row.window_mode !== 'tail')
65
77
  throw new ValidationError('tail response window_mode must equal tail');
78
+ strictWindowSeq(afterSeq, 'after_seq');
66
79
  const windowStart = strictWindowSeq(row.window_start_seq, 'window_start_seq');
67
80
  const head = strictWindowSeq(row.head_seq, 'head_seq');
68
81
  const covered = strictWindowSeq(row.covered_through_seq, 'covered_through_seq');
@@ -72,7 +85,21 @@ function validateTailPage(result, pageLimit) {
72
85
  throw new ValidationError('covered_through_seq must equal head_seq');
73
86
  if (head - windowStart + 1 > pageLimit)
74
87
  throw new ValidationError('tail proven window exceeds limit');
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))
@@ -257,12 +284,61 @@ export class V2E2EECoordinator {
257
284
  _initSessionPromise = null;
258
285
  targetSetCache = new Map();
259
286
  senderIKSyncFetches = new Map();
287
+ /** 服务端 ACK cursor 高于本地 A 时,延迟到下一次 Forward Pull 再恢复。 */
288
+ pendingForwardCursors = new Map();
289
+ /** 本地 A 已提交但服务端 ACK 尚未确认时,下一次 Forward Pull 必须继续推进。 */
290
+ pendingForwardAcks = new Map();
260
291
  constructor(runtime) {
261
292
  this.runtime = runtime;
262
293
  }
263
294
  get client() {
264
295
  return this.runtime.client;
265
296
  }
297
+ resetForwardCursorState() {
298
+ this.pendingForwardCursors.clear();
299
+ this.pendingForwardAcks.clear();
300
+ }
301
+ pendingForwardCursor(ns, localAck) {
302
+ if (!ns)
303
+ return 0;
304
+ const pending = this.pendingForwardCursors.get(ns) ?? 0;
305
+ if (pending <= localAck) {
306
+ if (pending > 0)
307
+ this.pendingForwardCursors.delete(ns);
308
+ return 0;
309
+ }
310
+ return pending;
311
+ }
312
+ recordForwardCursor(ns, cursor, localAck) {
313
+ if (!ns || !Number.isSafeInteger(cursor) || cursor <= localAck)
314
+ return;
315
+ this.pendingForwardCursors.set(ns, Math.max(this.pendingForwardCursors.get(ns) ?? 0, cursor));
316
+ }
317
+ clearForwardCursor(ns, committedAck) {
318
+ const pending = this.pendingForwardCursors.get(ns) ?? 0;
319
+ if (pending > 0 && committedAck >= pending)
320
+ this.pendingForwardCursors.delete(ns);
321
+ }
322
+ pendingForwardAck(ns, localAck) {
323
+ if (!ns)
324
+ return 0;
325
+ const pending = this.pendingForwardAcks.get(ns) ?? 0;
326
+ if (!Number.isSafeInteger(pending) || pending <= 0 || localAck <= 0 || pending > localAck) {
327
+ this.pendingForwardAcks.delete(ns);
328
+ return 0;
329
+ }
330
+ return pending;
331
+ }
332
+ recordForwardAck(ns, ackSeq) {
333
+ if (!ns || !Number.isSafeInteger(ackSeq) || ackSeq <= 0)
334
+ return;
335
+ this.pendingForwardAcks.set(ns, Math.max(this.pendingForwardAcks.get(ns) ?? 0, ackSeq));
336
+ }
337
+ clearForwardAck(ns, confirmedSeq) {
338
+ const pending = this.pendingForwardAcks.get(ns) ?? 0;
339
+ if (pending > 0 && confirmedSeq >= pending)
340
+ this.pendingForwardAcks.delete(ns);
341
+ }
266
342
  get bootstrapCache() {
267
343
  const client = this.client;
268
344
  if (!(client._v2BootstrapCache instanceof Map)) {
@@ -590,7 +666,8 @@ export class V2E2EECoordinator {
590
666
  createdAt: Date.now(),
591
667
  });
592
668
  client._clientLog.debug(`V2 decrypt pending sender IK: key=${messageKey} from=${fromAid} device=${senderDeviceId || '-'} group=${groupId || '<p2p>'} pending=${client._v2SenderIKPending.size}`);
593
- this.scheduleSenderIKFetch(fromAid, senderDeviceId, groupId);
669
+ if (args.startFetch !== false)
670
+ this.scheduleSenderIKFetch(fromAid, senderDeviceId, groupId);
594
671
  }
595
672
  scheduleSenderIKFetch(fromAid, senderDeviceId, groupId) {
596
673
  const client = this.client;
@@ -644,8 +721,9 @@ export class V2E2EECoordinator {
644
721
  const pendingItems = [...client._v2SenderIKPending.entries()].filter(([, entry]) => entry.fromAid === fromAid && entry.senderDeviceId === senderDeviceId && entry.groupId === groupId);
645
722
  for (const [key, entry] of pendingItems) {
646
723
  let plaintext = null;
724
+ const retryStatus = {};
647
725
  try {
648
- plaintext = await client._decryptV2Message(entry.msg, false);
726
+ plaintext = await client._decryptV2Message(entry.msg, false, true, true, true, retryStatus);
649
727
  }
650
728
  catch (exc) {
651
729
  client._clientLog.warn(`V2 sender IK pending retry raised: key=${key} err=${formatE2EEError(exc)}`);
@@ -653,6 +731,10 @@ export class V2E2EECoordinator {
653
731
  continue;
654
732
  }
655
733
  if (plaintext === null) {
734
+ if (retryStatus.deferred) {
735
+ client._clientLog.warn(`V2 pending retry still missing key material: key=${key}`);
736
+ continue;
737
+ }
656
738
  client._clientLog.warn(`V2 sender IK pending retry still undecryptable: key=${key}`);
657
739
  const isGroup = Boolean(entry.groupId);
658
740
  const eventName = isGroup ? 'group.message_undecryptable' : 'message.undecryptable';
@@ -904,7 +986,7 @@ export class V2E2EECoordinator {
904
986
  history_error: { code: 'decrypt_failed', message: reason },
905
987
  };
906
988
  }
907
- async decodeP2PWindowMessage(msg, publish, history, source) {
989
+ async decodeP2PWindowMessage(msg, publish, history, source, orderedPublish = false) {
908
990
  const client = this.client;
909
991
  const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
910
992
  let plaintext = null;
@@ -939,16 +1021,27 @@ export class V2E2EECoordinator {
939
1021
  }
940
1022
  if (publish) {
941
1023
  const event = client._delivery.p2pAppEventForMessage(plaintext);
942
- await client._publishAppEvent(event.event, event.payload, source);
1024
+ if (orderedPublish) {
1025
+ await client._delivery.publishOrderedMessage(event.event, client._aid ? `p2p:${client._aid}` : '', seq, event.payload);
1026
+ }
1027
+ else {
1028
+ await client._publishAppEvent(event.event, event.payload, source);
1029
+ }
943
1030
  }
944
1031
  return plaintext;
945
1032
  }
946
- async decodeGroupWindowMessage(msg, groupId, groupAid, publish, history, source) {
1033
+ async decodeGroupWindowMessage(msg, groupId, groupAid, publish, history, source, orderedPublish = false) {
947
1034
  const client = this.client;
948
1035
  const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
949
1036
  let plaintext = null;
950
1037
  if (String(msg.version ?? '') === 'v1') {
951
1038
  const payload = msg.payload;
1039
+ if (payload === undefined || payload === null) {
1040
+ if (history)
1041
+ return this.historyDecryptFailure(msg, 'legacy payload is missing');
1042
+ client._clientLog.warn(`Group Tail 缺少 payload 的 V1 行已跳过,连续性证明仍保留: group=${groupId}, seq=${seq}`);
1043
+ return null;
1044
+ }
952
1045
  if (isJsonObject(payload)
953
1046
  && ['e2ee.encrypted', 'e2ee.group_encrypted'].includes(String(payload.type ?? ''))) {
954
1047
  if (history)
@@ -984,8 +1077,14 @@ export class V2E2EECoordinator {
984
1077
  client._clientLog.warn(`Group Tail 单条消息解密失败,连续性证明仍保留: group=${groupId}, seq=${seq}`);
985
1078
  return null;
986
1079
  }
987
- if (publish)
988
- 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
+ }
989
1088
  return plaintext;
990
1089
  }
991
1090
  async applyTailFloor(ns, floor) {
@@ -994,9 +1093,9 @@ export class V2E2EECoordinator {
994
1093
  return;
995
1094
  client._seqTracker.forceContiguousSeq(ns, floor);
996
1095
  client._persistSeq(ns);
997
- client._saveSeqTrackerState?.();
1096
+ await client._saveSeqTrackerState?.();
998
1097
  }
999
- async pullV2TailInternal(params) {
1098
+ async pullV2TailInternal(params, deferPublish = false) {
1000
1099
  const client = this.client;
1001
1100
  if (params.window_mode !== 'tail')
1002
1101
  throw new ValidationError('window_mode must equal tail');
@@ -1009,7 +1108,7 @@ export class V2E2EECoordinator {
1009
1108
  const floor = Number(result.retention_floor_seq ?? 0);
1010
1109
  const previous = ns ? client._seqTracker.getSyncState(ns) : null;
1011
1110
  await this.applyTailFloor(ns, Number.isSafeInteger(floor) ? floor : 0);
1012
- const page = validateTailPage(result, limit);
1111
+ const page = validateTailPage(result, afterSeq, limit);
1013
1112
  if (ns) {
1014
1113
  client._seqTracker.commitTailWindow(ns, {
1015
1114
  windowStartSeq: page.windowStart,
@@ -1018,11 +1117,11 @@ export class V2E2EECoordinator {
1018
1117
  pageLimit: limit,
1019
1118
  });
1020
1119
  client._persistSeq(ns);
1021
- client._saveSeqTrackerState?.();
1120
+ await client._saveSeqTrackerState?.();
1022
1121
  }
1023
1122
  const decoded = [];
1024
1123
  for (const msg of page.messages) {
1025
- const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail');
1124
+ const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail', deferPublish);
1026
1125
  if (plaintext)
1027
1126
  decoded.push(plaintext);
1028
1127
  }
@@ -1067,13 +1166,18 @@ export class V2E2EECoordinator {
1067
1166
  }
1068
1167
  const decrypted = [];
1069
1168
  let nextAfterSeq = opts?.force ? afterSeq : (afterSeq || (ns ? client._seqTracker.getContiguousSeq(ns) : 0));
1169
+ let pullGateKey = ns
1170
+ ? `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`
1171
+ : '';
1172
+ const deferredServerCursor = ns ? this.pendingForwardCursor(ns, ns ? client._seqTracker.getContiguousSeq(ns) : 0) : 0;
1173
+ const deferredForwardAck = ns ? this.pendingForwardAck(ns, client._seqTracker.getContiguousSeq(ns)) : 0;
1070
1174
  let pageCount = 0;
1071
1175
  let lastAutoAckSeq = 0;
1072
- let pendingAckSeq = 0;
1176
+ let pendingAckSeq = deferredForwardAck;
1073
1177
  const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
1074
1178
  while (pageCount < maxPages) {
1075
1179
  pageCount += 1;
1076
- const ackUpToSeq = opts?.skipAutoAck ? 0 : pendingAckSeq;
1180
+ const ackUpToSeq = pendingAckSeq;
1077
1181
  const inlineAckGeneration = ackUpToSeq > 0
1078
1182
  ? client._delivery.captureP2PInlineAckGeneration()
1079
1183
  : null;
@@ -1085,15 +1189,29 @@ export class V2E2EECoordinator {
1085
1189
  ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
1086
1190
  });
1087
1191
  if (ackUpToSeq > 0) {
1088
- if (inlineAckGeneration !== null) {
1089
- client._delivery.onP2PV2PullAckResult(result, ackUpToSeq, inlineAckGeneration);
1192
+ const actualAckSeq = client._delivery.resolveP2PPullAckSeq(result, ackUpToSeq);
1193
+ if (actualAckSeq >= ackUpToSeq) {
1194
+ if (inlineAckGeneration !== null) {
1195
+ client._delivery.onP2PV2PullAckResult(result, ackUpToSeq, inlineAckGeneration);
1196
+ }
1197
+ pendingAckSeq = 0;
1198
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1199
+ this.clearForwardAck(ns, ackUpToSeq);
1200
+ }
1201
+ else {
1202
+ client._clientLog.warn(`message.v2.pull piggyback ACK below target: requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
1203
+ await this.confirmForwardP2PAck(ackUpToSeq);
1204
+ pendingAckSeq = 0;
1205
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1090
1206
  }
1091
- pendingAckSeq = 0;
1092
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1093
1207
  }
1094
1208
  const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1095
1209
  const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
1096
- 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 ?? '')}`);
1210
+ const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1211
+ const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
1212
+ const hasMore = result.has_more === true;
1213
+ const serverHead = safeNonNegativeSeq(cursor?.latest_seq) ?? 0;
1214
+ 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 ?? '')}`);
1097
1215
  for (const msg of messages) {
1098
1216
  client._logMessageDebug('pull-raw', 'message.v2.pull', 'message.received', msg);
1099
1217
  }
@@ -1108,6 +1226,8 @@ export class V2E2EECoordinator {
1108
1226
  if (seqs.length > 0) {
1109
1227
  pageMaxSeq = Math.max(...seqs);
1110
1228
  }
1229
+ const deferredKeyFetches = new Map();
1230
+ let blockedSeq = 0;
1111
1231
  for (const msg of messages) {
1112
1232
  const seq = Number(msg.seq ?? 0);
1113
1233
  if (!Number.isFinite(seq) || seq <= 0)
@@ -1133,7 +1253,7 @@ export class V2E2EECoordinator {
1133
1253
  attachGatewayProximity(v1Msg, msg);
1134
1254
  const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
1135
1255
  if (ns) {
1136
- await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload);
1256
+ await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
1137
1257
  }
1138
1258
  else {
1139
1259
  await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull');
@@ -1154,13 +1274,25 @@ export class V2E2EECoordinator {
1154
1274
  if (spkId && client._v2Session && !client._v2Session.isCurrentSPK(spkId)) {
1155
1275
  client._v2Session.trackOldSPKMaxSeq(spkId, seq);
1156
1276
  }
1157
- const plaintext = await client._decryptV2Message(msg);
1277
+ const deferStatus = {};
1278
+ const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true);
1279
+ if (deferStatus.deferred) {
1280
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1281
+ }
1282
+ if (deferStatus.deferred && deferStatus.fromAid) {
1283
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1284
+ deferredKeyFetches.set(key, {
1285
+ fromAid: deferStatus.fromAid,
1286
+ senderDeviceId: deferStatus.senderDeviceId ?? '',
1287
+ groupId: deferStatus.groupId ?? '',
1288
+ });
1289
+ }
1158
1290
  if (plaintext === null) {
1159
1291
  client._clientLog.debug(`message.v2.pull decrypt returned null: seq=${seq}, ns=${ns || '<none>'}`);
1160
1292
  continue;
1161
1293
  }
1162
1294
  if (ns) {
1163
- await client._publishPulledMessage('message.received', ns, seq, plaintext);
1295
+ await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1164
1296
  }
1165
1297
  else {
1166
1298
  await client._publishAppEvent('message.received', plaintext, 'pull');
@@ -1169,10 +1301,11 @@ export class V2E2EECoordinator {
1169
1301
  client._logMessageDebug('decrypt-ok', 'message.v2.pull', 'message.received', plaintext);
1170
1302
  }
1171
1303
  const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
1172
- const serverAckSeq = Number(result.server_ack_seq ?? 0);
1304
+ const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
1305
+ const serverAckSeq = parsedServerAckSeq ?? 0;
1173
1306
  const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
1174
1307
  if (ns) {
1175
- const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
1308
+ const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, blockedSeq > 0 ? blockedSeq - 1 : (seqs.length > 0 ? pageMaxSeq : pageContigBefore));
1176
1309
  let ackSeq = pageContigBefore;
1177
1310
  let contigAdvanced = false;
1178
1311
  try {
@@ -1181,6 +1314,9 @@ export class V2E2EECoordinator {
1181
1314
  if (retentionFloor > pageContigBefore) {
1182
1315
  client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${pageContigBefore} -> retention_floor_seq=${retentionFloor}`);
1183
1316
  }
1317
+ if (deferredServerCursor > pageContigBefore && pageCount === 1) {
1318
+ client._clientLog.info(`message.v2.pull restore local A from deferred server ACK cursor: ns=${ns}, contiguous=${pageContigBefore} -> server_ack_seq=${deferredServerCursor}`);
1319
+ }
1184
1320
  if (seqs.length > 0) {
1185
1321
  client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
1186
1322
  }
@@ -1188,10 +1324,9 @@ export class V2E2EECoordinator {
1188
1324
  ackSeq = client._seqTracker.getContiguousSeq(ns);
1189
1325
  contigAdvanced = ackSeq !== pageContigBefore;
1190
1326
  if (contigAdvanced) {
1191
- await client._drainOrderedMessages(ns, undefined, true);
1192
- ackSeq = client._seqTracker.getContiguousSeq(ns);
1193
- contigAdvanced = ackSeq !== pageContigBefore;
1194
- client._commitSeqTrackerState(ns);
1327
+ await client._commitSeqTrackerState(ns);
1328
+ if (pageCount === 1 && deferredServerCursor > 0)
1329
+ this.clearForwardCursor(ns, ackSeq);
1195
1330
  }
1196
1331
  }
1197
1332
  catch (exc) {
@@ -1201,14 +1336,27 @@ export class V2E2EECoordinator {
1201
1336
  client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
1202
1337
  throw exc;
1203
1338
  }
1204
- const ackNeeded = pageMessages.length > 0
1205
- && ackSeq > 0
1339
+ if (contigAdvanced)
1340
+ await client._drainOrderedMessages(ns, undefined, false, false);
1341
+ for (const fetch of deferredKeyFetches.values()) {
1342
+ this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1343
+ }
1344
+ if (parsedServerAckSeq !== null && serverAckSeq > ackSeq) {
1345
+ this.recordForwardCursor(ns, serverAckSeq, ackSeq);
1346
+ }
1347
+ // 无本页服务端水位时,只有本页成功提交了比已知服务端更高的 A
1348
+ // 才新增 ACK 义务。空页不能仅因本地已有 A 而重复 ACK。
1349
+ const knownServerAckSeq = hasServerAckSeq
1350
+ ? serverAckSeq
1351
+ : (pageCount === 1 ? deferredServerCursor : 0);
1352
+ const ackNeeded = ackSeq > 0
1206
1353
  && ackSeq > lastAutoAckSeq
1207
- && !opts?.skipAutoAck
1208
- && (contigAdvanced || (hasServerAckSeq && ackSeq > serverAckSeq));
1354
+ && ((hasServerAckSeq && ackSeq > serverAckSeq)
1355
+ || (contigAdvanced && ackSeq > knownServerAckSeq));
1209
1356
  if (ackNeeded) {
1357
+ this.recordForwardAck(ns, ackSeq);
1210
1358
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
1211
- const canContinuePage = messages.length > 0 && nextAfter > nextAfterSeq;
1359
+ const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq;
1212
1360
  if (canContinuePage) {
1213
1361
  pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
1214
1362
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
@@ -1216,18 +1364,35 @@ export class V2E2EECoordinator {
1216
1364
  }
1217
1365
  else {
1218
1366
  client._clientLog.debug(`message.v2.pull sending terminal auto-ack: ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
1219
- await this.ackV2(ackSeq);
1367
+ await this.confirmForwardP2PAck(ackSeq);
1220
1368
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1221
1369
  }
1222
1370
  }
1223
1371
  }
1372
+ if (!ns) {
1373
+ for (const fetch of deferredKeyFetches.values()) {
1374
+ this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1375
+ }
1376
+ }
1224
1377
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
1225
1378
  const rawCount = messages.length;
1226
- const canContinuePage = rawCount > 0 && nextAfter > nextAfterSeq;
1227
- const fullPage = canContinuePage && limit > 0 && rawCount >= limit;
1228
- const shouldContinue = canContinuePage && (fullPage || pendingAckSeq > 0);
1379
+ const shouldContinue = shouldContinueForwardPage({
1380
+ rawCount,
1381
+ nextAfterSeq,
1382
+ nextAfter,
1383
+ limit,
1384
+ hasMoreField,
1385
+ hasMore,
1386
+ serverHead,
1387
+ pendingAckSeq,
1388
+ });
1229
1389
  if (!shouldContinue)
1230
1390
  break;
1391
+ if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
1392
+ const nextKey = `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`;
1393
+ await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
1394
+ pullGateKey = nextKey;
1395
+ }
1231
1396
  const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
1232
1397
  if (tailDelayMs > 0)
1233
1398
  await v2Sleep(tailDelayMs);
@@ -1237,14 +1402,35 @@ export class V2E2EECoordinator {
1237
1402
  client._clientLog.warn(`message.v2.pull reached max_pages=${maxPages} after_seq=${nextAfterSeq}`);
1238
1403
  if (pendingAckSeq > 0) {
1239
1404
  client._clientLog.debug(`message.v2.pull flushing pending auto-ack after max_pages: ns=${ns}, ack_seq=${pendingAckSeq}`);
1240
- await this.ackV2(pendingAckSeq);
1405
+ await this.confirmForwardP2PAck(pendingAckSeq);
1241
1406
  pendingAckSeq = 0;
1242
1407
  }
1243
1408
  }
1244
1409
  client._clientLog.debug(`message.v2.pull done: requested_after_seq=${afterSeq}, pages=${pageCount}, decrypted=${decrypted.length}, ns=${ns || '<none>'}`);
1245
1410
  return decrypted;
1246
1411
  }
1412
+ async confirmForwardP2PAck(upToSeq) {
1413
+ const ns = this.client._aid ? `p2p:${this.client._aid}` : '';
1414
+ try {
1415
+ const result = await this.ackV2ForForward(upToSeq);
1416
+ const actualAckSeq = this.client._delivery.resolveForwardAckSeq(result, upToSeq);
1417
+ if (actualAckSeq < upToSeq) {
1418
+ throw new E2EEError(`message.v2.ack server ACK watermark ${actualAckSeq} is below requested ${upToSeq}`);
1419
+ }
1420
+ this.clearForwardAck(ns, upToSeq);
1421
+ }
1422
+ catch (exc) {
1423
+ this.recordForwardAck(ns, upToSeq);
1424
+ throw exc;
1425
+ }
1426
+ }
1247
1427
  async ackV2(upToSeq) {
1428
+ return this.ackV2WithConfirmation(upToSeq, false);
1429
+ }
1430
+ async ackV2ForForward(upToSeq) {
1431
+ return this.ackV2WithConfirmation(upToSeq, true);
1432
+ }
1433
+ async ackV2WithConfirmation(upToSeq, requireServerWatermark) {
1248
1434
  const client = this.client;
1249
1435
  const ns = client._aid ? `p2p:${client._aid}` : '';
1250
1436
  let seq = Number(upToSeq ?? (ns ? client._seqTracker.getContiguousSeq(ns) : 0));
@@ -1263,7 +1449,12 @@ export class V2E2EECoordinator {
1263
1449
  const result = isJsonObject(raw)
1264
1450
  ? { ...raw }
1265
1451
  : { result: raw };
1266
- const actualAckSeq = client._delivery.resolveActualAckSeq(result, seq);
1452
+ const actualAckSeq = requireServerWatermark
1453
+ ? client._delivery.resolveForwardAckSeq(result, seq)
1454
+ : client._delivery.resolveActualAckSeq(result, seq);
1455
+ if (requireServerWatermark && actualAckSeq < seq) {
1456
+ throw new E2EEError(`message.v2.ack server ACK watermark ${actualAckSeq} is below requested ${seq}`);
1457
+ }
1267
1458
  result.ack_seq = actualAckSeq;
1268
1459
  result.success = true;
1269
1460
  if (inlineAckGeneration !== null) {
@@ -1319,7 +1510,7 @@ export class V2E2EECoordinator {
1319
1510
  client._clientLog.debug(`group.v2.send ok: group=${gid}, use_cache=${useCache}, seq=${String((isJsonObject(result) ? result.seq : '') ?? '')}`);
1320
1511
  return result;
1321
1512
  };
1322
- const markSentSeq = (result) => {
1513
+ const markSentSeq = async (result) => {
1323
1514
  if (!isJsonObject(result))
1324
1515
  return;
1325
1516
  const obj = result;
@@ -1329,12 +1520,12 @@ export class V2E2EECoordinator {
1329
1520
  const ns = `group:${gid}`;
1330
1521
  client._seqTracker.onMessageSeq(ns, seq);
1331
1522
  client._markPublishedSeq(ns, seq);
1332
- client._saveSeqTrackerState();
1523
+ await client._saveSeqTrackerState();
1333
1524
  client._clientLog.debug(`group.v2.send marked own seq: group=${gid}, ns=${ns}, seq=${seq}`);
1334
1525
  };
1335
1526
  try {
1336
1527
  const result = await attempt(true);
1337
- markSentSeq(result);
1528
+ await markSentSeq(result);
1338
1529
  return client._delivery.attachSendResultEnvelope('group.send', withExplicitGroupAid({
1339
1530
  group_id: gid,
1340
1531
  payload,
@@ -1350,7 +1541,7 @@ export class V2E2EECoordinator {
1350
1541
  client._clientLog.debug(`V2 group speculative send rejected (code=${String(excCode)}), refreshing bootstrap`);
1351
1542
  this.deleteBootstrapCacheEntry(`group:${gid}`);
1352
1543
  const result = await attempt(false);
1353
- markSentSeq(result);
1544
+ await markSentSeq(result);
1354
1545
  return client._delivery.attachSendResultEnvelope('group.send', withExplicitGroupAid({
1355
1546
  group_id: gid,
1356
1547
  payload,
@@ -1509,7 +1700,7 @@ export class V2E2EECoordinator {
1509
1700
  });
1510
1701
  return envelope;
1511
1702
  }
1512
- async pullGroupV2TailInternal(params) {
1703
+ async pullGroupV2TailInternal(params, deferPublish = false) {
1513
1704
  const client = this.client;
1514
1705
  if (params.window_mode !== 'tail')
1515
1706
  throw new ValidationError('window_mode must equal tail');
@@ -1535,7 +1726,7 @@ export class V2E2EECoordinator {
1535
1726
  const floor = Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0);
1536
1727
  const previous = client._seqTracker.getSyncState(ns);
1537
1728
  await this.applyTailFloor(ns, Number.isSafeInteger(floor) ? floor : 0);
1538
- const page = validateTailPage(result, limit);
1729
+ const page = validateTailPage(result, afterSeq, limit);
1539
1730
  client._seqTracker.commitTailWindow(ns, {
1540
1731
  windowStartSeq: page.windowStart,
1541
1732
  headSeq: page.head,
@@ -1543,10 +1734,11 @@ export class V2E2EECoordinator {
1543
1734
  pageLimit: limit,
1544
1735
  });
1545
1736
  client._persistSeq(ns);
1546
- client._saveSeqTrackerState?.();
1737
+ await client._saveSeqTrackerState?.();
1547
1738
  const decoded = [];
1548
1739
  for (const msg of page.messages) {
1549
- const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, true, false, 'tail');
1740
+ client._logMessageDebug('tail-raw', 'group.v2.pull', 'group.message_created', msg);
1741
+ const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, true, false, 'tail', deferPublish);
1550
1742
  if (plaintext)
1551
1743
  decoded.push(plaintext);
1552
1744
  }
@@ -1625,12 +1817,19 @@ export class V2E2EECoordinator {
1625
1817
  const wireGroupId = String(opts?.wireGroupId ?? groupId ?? '').trim() || gid;
1626
1818
  const cursorParams = opts?.cursorParams ?? {};
1627
1819
  const ownsCursor = opts?.ownsCursor !== false;
1820
+ let pullGateKey = ownsCursor
1821
+ ? `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`
1822
+ : '';
1628
1823
  let nextAfterSeq = (opts?.explicitAfterSeq || opts?.force)
1629
1824
  ? afterSeq
1630
1825
  : (afterSeq || client._seqTracker.getContiguousSeq(ns));
1826
+ const deferredServerCursor = ownsCursor
1827
+ ? this.pendingForwardCursor(ns, client._seqTracker.getContiguousSeq(ns))
1828
+ : 0;
1829
+ const deferredForwardAck = ownsCursor ? this.pendingForwardAck(ns, client._seqTracker.getContiguousSeq(ns)) : 0;
1631
1830
  let pageCount = 0;
1632
1831
  let lastAutoAckSeq = 0;
1633
- let pendingAckSeq = 0;
1832
+ let pendingAckSeq = deferredForwardAck;
1634
1833
  const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
1635
1834
  while (pageCount < maxPages) {
1636
1835
  pageCount += 1;
@@ -1648,11 +1847,21 @@ export class V2E2EECoordinator {
1648
1847
  ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
1649
1848
  }, groupAid));
1650
1849
  if (ackUpToSeq > 0) {
1651
- if (inlineAckGeneration !== null) {
1652
- client._delivery.onGroupV2PullAckResult(gid, result, ackUpToSeq, inlineAckGeneration);
1850
+ const actualAckSeq = client._delivery.resolveGroupPullAckSeq(result, ackUpToSeq);
1851
+ if (actualAckSeq >= ackUpToSeq) {
1852
+ if (inlineAckGeneration !== null) {
1853
+ client._delivery.onGroupV2PullAckResult(gid, result, ackUpToSeq, inlineAckGeneration);
1854
+ }
1855
+ pendingAckSeq = 0;
1856
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1857
+ this.clearForwardAck(ns, ackUpToSeq);
1858
+ }
1859
+ else {
1860
+ client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
1861
+ await this.confirmForwardGroupAck(gid, ackUpToSeq, groupAid);
1862
+ pendingAckSeq = 0;
1863
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1653
1864
  }
1654
- pendingAckSeq = 0;
1655
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1656
1865
  }
1657
1866
  const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
1658
1867
  if (resultGroupAid)
@@ -1661,7 +1870,10 @@ export class V2E2EECoordinator {
1661
1870
  const pageMessages = (Array.isArray(result.messages) ? result.messages : []);
1662
1871
  const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
1663
1872
  const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1664
- 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 ?? '')}, cursor_current=${String(cursor?.current_seq ?? '')}`);
1873
+ const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
1874
+ const hasMore = result.has_more === true;
1875
+ const serverHead = Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0);
1876
+ 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 ?? '')}`);
1665
1877
  for (const msg of messages) {
1666
1878
  client._logMessageDebug('pull-raw', 'group.v2.pull', 'group.message_created', msg);
1667
1879
  }
@@ -1676,6 +1888,8 @@ export class V2E2EECoordinator {
1676
1888
  if (seqs.length > 0) {
1677
1889
  pageMaxSeq = Math.max(...seqs);
1678
1890
  }
1891
+ const deferredKeyFetches = new Map();
1892
+ let blockedSeq = 0;
1679
1893
  for (const msg of messages) {
1680
1894
  const seq = Number(msg.seq ?? 0);
1681
1895
  if (!Number.isFinite(seq) || seq <= 0)
@@ -1711,7 +1925,7 @@ export class V2E2EECoordinator {
1711
1925
  };
1712
1926
  v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
1713
1927
  attachGatewayProximity(v1Msg, msg);
1714
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg);
1928
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
1715
1929
  decrypted.push(v1Msg);
1716
1930
  client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
1717
1931
  continue;
@@ -1731,7 +1945,7 @@ export class V2E2EECoordinator {
1731
1945
  };
1732
1946
  v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
1733
1947
  attachGatewayProximity(v1Msg, msg);
1734
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg);
1948
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
1735
1949
  decrypted.push(v1Msg);
1736
1950
  client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
1737
1951
  continue;
@@ -1743,7 +1957,19 @@ export class V2E2EECoordinator {
1743
1957
  client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
1744
1958
  continue;
1745
1959
  }
1746
- let plaintext = await client._decryptV2Message(msg);
1960
+ const deferStatus = {};
1961
+ let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true);
1962
+ if (deferStatus.deferred) {
1963
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1964
+ }
1965
+ if (deferStatus.deferred && deferStatus.fromAid) {
1966
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1967
+ deferredKeyFetches.set(key, {
1968
+ fromAid: deferStatus.fromAid,
1969
+ senderDeviceId: deferStatus.senderDeviceId ?? '',
1970
+ groupId: deferStatus.groupId ?? '',
1971
+ });
1972
+ }
1747
1973
  if (plaintext === null) {
1748
1974
  client._clientLog.debug(`group.v2.pull decrypt returned null: group=${gid}, seq=${seq}`);
1749
1975
  continue;
@@ -1751,52 +1977,71 @@ export class V2E2EECoordinator {
1751
1977
  plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
1752
1978
  plaintext.group_id = gid;
1753
1979
  plaintext.group_aid = eventGroupAid;
1754
- await client._publishPulledMessage('group.message_created', ns, seq, plaintext);
1980
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
1755
1981
  decrypted.push(plaintext);
1756
1982
  client._logMessageDebug('decrypt-ok', 'group.v2.pull', 'group.message_created', plaintext);
1757
1983
  }
1758
- const cursorCurrentSeq = Number(cursor?.current_seq ?? 0);
1759
1984
  const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
1985
+ const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
1986
+ const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
1760
1987
  const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq'));
1761
1988
  const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
1762
1989
  const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
1763
- const commitTarget = Math.max(pageContigBefore, effectiveFloor, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
1990
+ const commitTarget = Math.max(pageContigBefore, effectiveFloor, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, blockedSeq > 0 ? blockedSeq - 1 : (seqs.length > 0 ? pageMaxSeq : pageContigBefore));
1764
1991
  let ackSeq = pageContigBefore;
1765
1992
  let contigAdvanced = false;
1766
- try {
1767
- if (commitTarget > pageContigBefore) {
1768
- client._seqTracker.forceContiguousSeq(ns, commitTarget);
1769
- if (effectiveFloor > pageContigBefore) {
1770
- client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
1771
- }
1772
- if (seqs.length > 0) {
1773
- client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
1993
+ if (ownsCursor) {
1994
+ try {
1995
+ if (commitTarget > pageContigBefore) {
1996
+ client._seqTracker.forceContiguousSeq(ns, commitTarget);
1997
+ if (effectiveFloor > pageContigBefore) {
1998
+ client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
1999
+ }
2000
+ if (deferredServerCursor > pageContigBefore && pageCount === 1) {
2001
+ client._clientLog.info(`group.v2.pull restore local A from deferred server ACK cursor: group=${gid}, ns=${ns}, contiguous=${pageContigBefore} -> current_seq=${deferredServerCursor}`);
2002
+ }
2003
+ if (seqs.length > 0) {
2004
+ client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
2005
+ }
1774
2006
  }
1775
- }
1776
- ackSeq = client._seqTracker.getContiguousSeq(ns);
1777
- contigAdvanced = ackSeq !== pageContigBefore;
1778
- if (contigAdvanced) {
1779
- await client._drainOrderedMessages(ns, undefined, true);
1780
2007
  ackSeq = client._seqTracker.getContiguousSeq(ns);
1781
2008
  contigAdvanced = ackSeq !== pageContigBefore;
1782
- client._commitSeqTrackerState(ns);
2009
+ if (contigAdvanced) {
2010
+ await client._commitSeqTrackerState(ns);
2011
+ if (pageCount === 1 && deferredServerCursor > 0)
2012
+ this.clearForwardCursor(ns, ackSeq);
2013
+ }
1783
2014
  }
1784
- }
1785
- catch (exc) {
1786
- if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
1787
- client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
2015
+ catch (exc) {
2016
+ if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
2017
+ client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
2018
+ }
2019
+ client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
2020
+ throw exc;
1788
2021
  }
1789
- client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
1790
- throw exc;
2022
+ if (contigAdvanced)
2023
+ await client._drainOrderedMessages(ns, undefined, false, false);
2024
+ }
2025
+ for (const fetch of deferredKeyFetches.values()) {
2026
+ this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1791
2027
  }
1792
- const ackNeeded = pageMessages.length > 0
1793
- && ackSeq > 0
2028
+ if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
2029
+ this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
2030
+ }
2031
+ // 无本页 cursor.current_seq 时,只有本页成功提交了比已知 cursor
2032
+ // 更高的 A 才新增 ACK 义务;空页不得因已有本地 A 重复确认。
2033
+ const knownServerCursorSeq = hasServerCursor
2034
+ ? cursorCurrentSeq
2035
+ : (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
2036
+ const ackNeeded = ackSeq > 0
1794
2037
  && ackSeq > lastAutoAckSeq
1795
2038
  && ownsCursor
1796
- && (contigAdvanced || (hasServerCursor && ackSeq > cursorCurrentSeq));
2039
+ && ((hasServerCursor && ackSeq > cursorCurrentSeq)
2040
+ || (contigAdvanced && ackSeq > knownServerCursorSeq));
1797
2041
  if (ackNeeded) {
2042
+ this.recordForwardAck(ns, ackSeq);
1798
2043
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
1799
- const canContinuePage = messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
2044
+ const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
1800
2045
  if (canContinuePage) {
1801
2046
  pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
1802
2047
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
@@ -1804,7 +2049,7 @@ export class V2E2EECoordinator {
1804
2049
  }
1805
2050
  else {
1806
2051
  client._clientLog.debug(`group.v2.pull sending terminal auto-ack: group=${gid}, ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
1807
- await this.ackGroupV2(gid, ackSeq);
2052
+ await this.confirmForwardGroupAck(gid, ackSeq, groupAid);
1808
2053
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1809
2054
  }
1810
2055
  }
@@ -1812,11 +2057,23 @@ export class V2E2EECoordinator {
1812
2057
  if (!ownsCursor)
1813
2058
  break;
1814
2059
  const rawCount = messages.length;
1815
- const canContinuePage = rawCount > 0 && nextAfter > nextAfterSeq;
1816
- const fullPage = canContinuePage && limit > 0 && rawCount >= limit;
1817
- const shouldContinue = canContinuePage && (fullPage || pendingAckSeq > 0);
2060
+ const shouldContinue = shouldContinueForwardPage({
2061
+ rawCount,
2062
+ nextAfterSeq,
2063
+ nextAfter,
2064
+ limit,
2065
+ hasMoreField,
2066
+ hasMore,
2067
+ serverHead,
2068
+ pendingAckSeq,
2069
+ });
1818
2070
  if (!shouldContinue)
1819
2071
  break;
2072
+ if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
2073
+ const nextKey = `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`;
2074
+ await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
2075
+ pullGateKey = nextKey;
2076
+ }
1820
2077
  const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
1821
2078
  if (tailDelayMs > 0)
1822
2079
  await v2Sleep(tailDelayMs);
@@ -1826,14 +2083,35 @@ export class V2E2EECoordinator {
1826
2083
  client._clientLog.warn(`group.v2.pull reached max_pages=${maxPages} group=${gid} after_seq=${nextAfterSeq}`);
1827
2084
  if (pendingAckSeq > 0) {
1828
2085
  client._clientLog.debug(`group.v2.pull flushing pending auto-ack after max_pages: group=${gid}, ns=${ns}, ack_seq=${pendingAckSeq}`);
1829
- await this.ackGroupV2(gid, pendingAckSeq);
2086
+ await this.confirmForwardGroupAck(gid, pendingAckSeq, groupAid);
1830
2087
  pendingAckSeq = 0;
1831
2088
  }
1832
2089
  }
1833
2090
  client._clientLog.debug(`group.v2.pull done: group=${gid}, requested_after_seq=${afterSeq}, pages=${pageCount}, decrypted=${decrypted.length}, ns=${ns}`);
1834
2091
  return decrypted;
1835
2092
  }
2093
+ async confirmForwardGroupAck(groupId, upToSeq, groupAid) {
2094
+ const ns = `group:${String(groupId ?? '').trim()}`;
2095
+ try {
2096
+ const result = await this.ackGroupV2ForForward(groupId, upToSeq, groupAid);
2097
+ const actualAckSeq = this.client._delivery.resolveForwardAckSeq(result, upToSeq);
2098
+ if (actualAckSeq < upToSeq) {
2099
+ throw new E2EEError(`group.v2.ack server ACK watermark ${actualAckSeq} is below requested ${upToSeq}`);
2100
+ }
2101
+ this.clearForwardAck(ns, upToSeq);
2102
+ }
2103
+ catch (exc) {
2104
+ this.recordForwardAck(ns, upToSeq);
2105
+ throw exc;
2106
+ }
2107
+ }
1836
2108
  async ackGroupV2(groupId, upToSeq, groupAid) {
2109
+ return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, false);
2110
+ }
2111
+ async ackGroupV2ForForward(groupId, upToSeq, groupAid) {
2112
+ return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, true);
2113
+ }
2114
+ async ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, requireServerWatermark) {
1837
2115
  const client = this.client;
1838
2116
  const gid = String(groupId ?? '').trim();
1839
2117
  if (!gid)
@@ -1852,7 +2130,12 @@ export class V2E2EECoordinator {
1852
2130
  const inlineAckGeneration = client._delivery.captureGroupInlineAckGeneration();
1853
2131
  client._clientLog.debug(`group.v2.ack send: group=${gid}, ns=${ns}, up_to_seq=${seq}`);
1854
2132
  const result = await client._callRawV2Rpc('group.v2.ack', withExplicitGroupAid({ group_id: gid, up_to_seq: seq }, explicitGroupAid));
1855
- const actualAckSeq = client._delivery.resolveActualAckSeq(result, seq);
2133
+ const actualAckSeq = requireServerWatermark
2134
+ ? client._delivery.resolveForwardAckSeq(result, seq)
2135
+ : client._delivery.resolveActualAckSeq(result, seq);
2136
+ if (requireServerWatermark && actualAckSeq < seq) {
2137
+ throw new E2EEError(`group.v2.ack server ACK watermark ${actualAckSeq} is below requested ${seq}`);
2138
+ }
1856
2139
  if (inlineAckGeneration !== null) {
1857
2140
  client._delivery.onGroupV2AckSucceeded(gid, actualAckSeq, inlineAckGeneration);
1858
2141
  }