@agentunion/fastaun-browser 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 (104) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/_packed_docs/CHANGELOG.md +104 -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 +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 +230 -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 +24 -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 +30253 -21955
  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 +40 -5
  55. package/dist/client/delivery.d.ts.map +1 -1
  56. package/dist/client/delivery.js +719 -221
  57. package/dist/client/delivery.js.map +1 -1
  58. package/dist/client/group-state.js +2 -2
  59. package/dist/client/group-state.js.map +1 -1
  60. package/dist/client/lifecycle.d.ts +15 -1
  61. package/dist/client/lifecycle.d.ts.map +1 -1
  62. package/dist/client/lifecycle.js +463 -151
  63. package/dist/client/lifecycle.js.map +1 -1
  64. package/dist/client/rpc-pipeline.d.ts +25 -6
  65. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  66. package/dist/client/rpc-pipeline.js +407 -123
  67. package/dist/client/rpc-pipeline.js.map +1 -1
  68. package/dist/client/runtime.d.ts +1 -3
  69. package/dist/client/runtime.d.ts.map +1 -1
  70. package/dist/client/runtime.js +4 -7
  71. package/dist/client/runtime.js.map +1 -1
  72. package/dist/client/v2-e2ee.d.ts +20 -2
  73. package/dist/client/v2-e2ee.d.ts.map +1 -1
  74. package/dist/client/v2-e2ee.js +434 -112
  75. package/dist/client/v2-e2ee.js.map +1 -1
  76. package/dist/client.d.ts +22 -10
  77. package/dist/client.d.ts.map +1 -1
  78. package/dist/client.js +500 -116
  79. package/dist/client.js.map +1 -1
  80. package/dist/facades.d.ts +23 -1
  81. package/dist/facades.d.ts.map +1 -1
  82. package/dist/facades.js +55 -7
  83. package/dist/facades.js.map +1 -1
  84. package/dist/group-id.d.ts.map +1 -1
  85. package/dist/group-id.js +2 -17
  86. package/dist/group-id.js.map +1 -1
  87. package/dist/index.d.ts +4 -3
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +2 -1
  90. package/dist/index.js.map +1 -1
  91. package/dist/seq-tracker.d.ts.map +1 -1
  92. package/dist/seq-tracker.js +8 -2
  93. package/dist/seq-tracker.js.map +1 -1
  94. package/dist/tools/cross-sdk-agent.js +23 -1
  95. package/dist/tools/cross-sdk-agent.js.map +1 -1
  96. package/dist/transport.d.ts.map +1 -1
  97. package/dist/transport.js +8 -1
  98. package/dist/transport.js.map +1 -1
  99. package/dist/validators.d.ts.map +1 -1
  100. package/dist/validators.js +8 -14
  101. package/dist/validators.js.map +1 -1
  102. package/dist/version.d.ts +1 -1
  103. package/dist/version.js +1 -1
  104. package/package.json +6 -5
@@ -4,6 +4,10 @@ import { decryptMessage, encryptGroupMessage, encryptP2PMessage, } from '../v2/e
4
4
  import { V2Session, V2KeyStore } from '../v2/session/index.js';
5
5
  import { ConnectionState, isJsonObject } from '../types.js';
6
6
  import { normalizeGroupMentionMode } from './mention-mode.js';
7
+ function pullGateKeyForClient(client, method, params, fallback) {
8
+ const key = client._rpcPipeline?.pullGateKeyForCall?.(method, params);
9
+ return typeof key === 'string' && key ? key : fallback;
10
+ }
7
11
  const V2_BOOTSTRAP_TTL_MS = 60 * 60 * 1000;
8
12
  const V2_RETRYABLE_CODES = new Set([-33011, -33012, -33050, -33052, -33054]);
9
13
  const V2_GROUP_STALE_BOOTSTRAP_CODE = -33054;
@@ -21,6 +25,9 @@ function strictWindowSeq(value, field, positive = false) {
21
25
  throw new ValidationError(`${field} must be >= ${minimum}`);
22
26
  return value;
23
27
  }
28
+ function safeNonNegativeSeq(value) {
29
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
30
+ }
24
31
  function strictWindowLimit(value) {
25
32
  const limit = strictWindowSeq(value, 'limit', true);
26
33
  if (limit > V2_PULL_MAX_PAGE_LIMIT)
@@ -53,26 +60,50 @@ function deduplicateForwardPageMessages(value, afterSeq) {
53
60
  if (!isJsonObject(raw))
54
61
  continue;
55
62
  const row = raw;
56
- const seq = Number(row.seq ?? 0);
57
- if (!Number.isSafeInteger(seq) || seq <= afterSeq || bySeq.has(seq))
63
+ const seq = safeNonNegativeSeq(row.seq);
64
+ if (seq === null || seq <= 0 || seq <= afterSeq || bySeq.has(seq))
58
65
  continue;
59
66
  bySeq.set(seq, row);
60
67
  }
61
68
  return [...bySeq.entries()].sort(([a], [b]) => a - b).map(([, row]) => row);
62
69
  }
63
- function validateTailPage(result, pageLimit) {
70
+ function shouldContinueForwardPage({ rawCount, nextAfterSeq, nextAfter, limit, hasMoreField, hasMore, serverHead, pendingAckSeq, }) {
71
+ // 无新 seq 或游标未前进时必须停止,避免错误元数据造成循环。
72
+ if (rawCount <= 0 || nextAfter <= nextAfterSeq)
73
+ return false;
74
+ if (hasMore || serverHead > nextAfter || pendingAckSeq > 0)
75
+ return true;
76
+ // 仅旧服务缺失 has_more 时,以满页保留兼容续拉。
77
+ return !hasMoreField && limit > 0 && rawCount >= limit;
78
+ }
79
+ function validateTailPage(result, afterSeq, pageLimit) {
64
80
  if (!isJsonObject(result))
65
81
  throw new ValidationError('tail response must be an object');
66
82
  const row = result;
67
83
  if (row.window_mode !== 'tail')
68
84
  throw new ValidationError('tail response window_mode must equal tail');
85
+ strictWindowSeq(afterSeq, 'after_seq');
69
86
  const windowStart = strictWindowSeq(row.window_start_seq, 'window_start_seq');
70
87
  const head = strictWindowSeq(row.head_seq, 'head_seq');
71
88
  const covered = strictWindowSeq(row.covered_through_seq, 'covered_through_seq');
72
89
  if (windowStart > head || covered !== head || head - windowStart + 1 > pageLimit) {
73
90
  throw new ValidationError('tail response proof is invalid');
74
91
  }
75
- return { windowStart, head, messages: deduplicateWindowMessages(row.messages, windowStart, head, 'the proven tail window') };
92
+ let lowerSeq = windowStart;
93
+ if (head <= afterSeq) {
94
+ if (windowStart !== head)
95
+ throw new ValidationError('empty tail window must end at head_seq');
96
+ lowerSeq = head + 1;
97
+ }
98
+ else {
99
+ if (windowStart < afterSeq + 1)
100
+ throw new ValidationError('tail window_start_seq is below after_seq');
101
+ lowerSeq = Math.max(windowStart, afterSeq + 1);
102
+ }
103
+ const messages = deduplicateWindowMessages(row.messages, lowerSeq, head, 'the proven tail window');
104
+ if (head <= afterSeq && messages.length > 0)
105
+ throw new ValidationError('tail response must not include messages at or below after_seq');
106
+ return { windowStart, head, messages };
76
107
  }
77
108
  function validateHistoryPage(result, beforeSeq) {
78
109
  if (!isJsonObject(result))
@@ -329,6 +360,9 @@ export class V2E2EECoordinator {
329
360
  runtime;
330
361
  targetSetCache = new Map();
331
362
  senderIKSyncFetches = new Map();
363
+ /** 服务端 ACK cursor 高于本地 A 时,延迟到下一次 Forward Pull 再恢复。 */
364
+ pendingForwardCursors = new Map();
365
+ pendingForwardAcks = new Map();
332
366
  v2SessionInitRetryTimer = null;
333
367
  v2SessionInitRetryBackgroundSync = false;
334
368
  v2SessionInitRetryIdentityKey = null;
@@ -338,6 +372,51 @@ export class V2E2EECoordinator {
338
372
  get client() {
339
373
  return this.runtime.client;
340
374
  }
375
+ resetForwardCursorState() {
376
+ this.pendingForwardCursors.clear();
377
+ this.pendingForwardAcks.clear();
378
+ }
379
+ pendingForwardCursor(ns, localAck) {
380
+ if (!ns)
381
+ return 0;
382
+ const pending = this.pendingForwardCursors.get(ns) ?? 0;
383
+ if (pending <= localAck) {
384
+ if (pending > 0)
385
+ this.pendingForwardCursors.delete(ns);
386
+ return 0;
387
+ }
388
+ return pending;
389
+ }
390
+ recordForwardCursor(ns, cursor, localAck) {
391
+ if (!ns || !Number.isSafeInteger(cursor) || cursor <= localAck)
392
+ return;
393
+ this.pendingForwardCursors.set(ns, Math.max(this.pendingForwardCursors.get(ns) ?? 0, cursor));
394
+ }
395
+ clearForwardCursor(ns, committedAck) {
396
+ const pending = this.pendingForwardCursors.get(ns) ?? 0;
397
+ if (pending > 0 && committedAck >= pending)
398
+ this.pendingForwardCursors.delete(ns);
399
+ }
400
+ pendingForwardAck(ns, localAck) {
401
+ if (!ns)
402
+ return 0;
403
+ const pending = this.pendingForwardAcks.get(ns) ?? 0;
404
+ if (!Number.isSafeInteger(pending) || pending <= 0 || localAck <= 0 || pending > localAck) {
405
+ this.pendingForwardAcks.delete(ns);
406
+ return 0;
407
+ }
408
+ return pending;
409
+ }
410
+ recordForwardAck(ns, ackSeq) {
411
+ if (!ns || !Number.isSafeInteger(ackSeq) || ackSeq <= 0)
412
+ return;
413
+ this.pendingForwardAcks.set(ns, Math.max(this.pendingForwardAcks.get(ns) ?? 0, ackSeq));
414
+ }
415
+ clearForwardAck(ns, confirmedSeq) {
416
+ const pending = this.pendingForwardAcks.get(ns) ?? 0;
417
+ if (pending > 0 && confirmedSeq >= pending)
418
+ this.pendingForwardAcks.delete(ns);
419
+ }
341
420
  get bootstrapCache() {
342
421
  const client = this.client;
343
422
  if (!(client._v2BootstrapCache instanceof Map)) {
@@ -759,7 +838,8 @@ export class V2E2EECoordinator {
759
838
  createdAt: Date.now(),
760
839
  });
761
840
  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);
841
+ if (args.startFetch !== false)
842
+ this.scheduleSenderIKFetch(fromAid, senderDeviceId, groupId);
763
843
  }
764
844
  scheduleSenderIKFetch(fromAid, senderDeviceId, groupId) {
765
845
  const client = this.client;
@@ -810,8 +890,9 @@ export class V2E2EECoordinator {
810
890
  const pendingItems = [...client._v2SenderIKPending.entries()].filter(([, entry]) => entry.fromAid === fromAid && entry.senderDeviceId === senderDeviceId && entry.groupId === groupId);
811
891
  for (const [key, entry] of pendingItems) {
812
892
  let plaintext = null;
893
+ const retryStatus = {};
813
894
  try {
814
- plaintext = await client._decryptV2Message(entry.msg, false);
895
+ plaintext = await client._decryptV2Message(entry.msg, false, true, true, true, retryStatus);
815
896
  }
816
897
  catch (exc) {
817
898
  client._clientLog.warn(`V2 sender IK pending retry raised: key=${key} err=${String(formatE2EEError(exc))}`);
@@ -819,6 +900,10 @@ export class V2E2EECoordinator {
819
900
  continue;
820
901
  }
821
902
  if (plaintext === null) {
903
+ if (retryStatus.deferred) {
904
+ client._clientLog.warn(`V2 pending retry still missing key material: key=${key}`);
905
+ continue;
906
+ }
822
907
  client._clientLog.warn(`V2 sender IK pending retry still undecryptable: key=${key}`);
823
908
  const isGroup = Boolean(entry.groupId);
824
909
  const eventName = isGroup ? 'group.message_undecryptable' : 'message.undecryptable';
@@ -907,7 +992,7 @@ export class V2E2EECoordinator {
907
992
  history_error: { code: 'decrypt_failed', message: reason },
908
993
  };
909
994
  }
910
- async decodeP2PWindowMessage(msg, publish, history, source) {
995
+ async decodeP2PWindowMessage(msg, publish, history, source, orderedPublish = false) {
911
996
  const client = this.client;
912
997
  const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
913
998
  let plaintext = null;
@@ -939,16 +1024,27 @@ export class V2E2EECoordinator {
939
1024
  }
940
1025
  if (publish) {
941
1026
  const event = client._delivery.p2pAppEventForMessage(plaintext);
942
- await client._publishAppEvent(event.event, event.payload, source);
1027
+ if (orderedPublish) {
1028
+ await client._delivery.publishOrderedMessage(event.event, client._aid ? `p2p:${client._aid}` : '', seq, event.payload);
1029
+ }
1030
+ else {
1031
+ await client._publishAppEvent(event.event, event.payload, source);
1032
+ }
943
1033
  }
944
1034
  return plaintext;
945
1035
  }
946
- async decodeGroupWindowMessage(msg, groupId, groupAid, publish, history, source) {
1036
+ async decodeGroupWindowMessage(msg, groupId, groupAid, publish, history, source, orderedPublish = false) {
947
1037
  const client = this.client;
948
1038
  const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
949
1039
  let plaintext = null;
950
1040
  if (String(msg.version ?? '') === 'v1') {
951
1041
  const payload = msg.payload;
1042
+ if (payload === undefined || payload === null) {
1043
+ if (history)
1044
+ return this.historyDecryptFailure(msg, 'legacy payload is missing');
1045
+ client._clientLog.warn(`Group Tail 缺少 payload 的 V1 行已跳过,连续性证明仍保留: group=${groupId}, seq=${seq}`);
1046
+ return null;
1047
+ }
952
1048
  if (isJsonObject(payload)
953
1049
  && ['e2ee.encrypted', 'e2ee.group_encrypted'].includes(String(payload.type ?? ''))) {
954
1050
  if (history)
@@ -985,8 +1081,14 @@ export class V2E2EECoordinator {
985
1081
  client._clientLog.warn(`Group Tail 单条消息解密失败,连续性证明仍保留: group=${groupId}, seq=${seq}`);
986
1082
  return null;
987
1083
  }
988
- if (publish)
989
- await client._publishAppEvent('group.message_created', plaintext, source);
1084
+ if (publish) {
1085
+ if (orderedPublish) {
1086
+ await client._delivery.publishOrderedMessage('group.message_created', `group:${groupId}`, seq, plaintext);
1087
+ }
1088
+ else {
1089
+ await client._publishAppEvent('group.message_created', plaintext, source);
1090
+ }
1091
+ }
990
1092
  return plaintext;
991
1093
  }
992
1094
  async decodeInlineP2PMessage(msg) {
@@ -1003,7 +1105,7 @@ export class V2E2EECoordinator {
1003
1105
  client._persistSeq(ns);
1004
1106
  await client._saveSeqTrackerState?.();
1005
1107
  }
1006
- async pullV2TailInternal(params) {
1108
+ async pullV2TailInternal(params, deferPublish = false) {
1007
1109
  const client = this.client;
1008
1110
  if (params.window_mode !== 'tail')
1009
1111
  throw new ValidationError('window_mode must equal tail');
@@ -1016,7 +1118,7 @@ export class V2E2EECoordinator {
1016
1118
  const floor = Number(result.retention_floor_seq ?? 0);
1017
1119
  const previous = ns ? client._seqTracker.getSyncState(ns) : null;
1018
1120
  await this.applyTailFloor(ns, Number.isSafeInteger(floor) ? floor : 0);
1019
- const page = validateTailPage(result, limit);
1121
+ const page = validateTailPage(result, afterSeq, limit);
1020
1122
  if (ns) {
1021
1123
  client._seqTracker.commitTailWindow(ns, {
1022
1124
  windowStartSeq: page.windowStart, headSeq: page.head, coveredThroughSeq: page.head, pageLimit: limit,
@@ -1026,7 +1128,7 @@ export class V2E2EECoordinator {
1026
1128
  }
1027
1129
  const decoded = [];
1028
1130
  for (const msg of page.messages) {
1029
- const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail');
1131
+ const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail', deferPublish);
1030
1132
  if (plaintext)
1031
1133
  decoded.push(plaintext);
1032
1134
  }
@@ -1053,14 +1155,39 @@ export class V2E2EECoordinator {
1053
1155
  await client._ensureV2SessionReady('message.pull');
1054
1156
  const ns = client._aid ? `p2p:${client._aid}` : '';
1055
1157
  if (opts?.windowMode === 'tail') {
1158
+ if (!opts.gateLocked) {
1159
+ const key = pullGateKeyForClient(client, 'message.v2.pull', {
1160
+ window_mode: 'tail', after_seq: afterSeq, limit,
1161
+ }, `${ns}|tail|after=${afterSeq}|limit=${limit}`);
1162
+ return await client._runPullSerialized(key, () => this.pullV2(afterSeq, limit, {
1163
+ ...(opts ?? {}),
1164
+ gateLocked: true,
1165
+ }), false);
1166
+ }
1056
1167
  const result = await this.pullV2TailInternal({ window_mode: 'tail', after_seq: afterSeq, limit });
1057
1168
  return Array.isArray(result.messages) ? result.messages : [];
1058
1169
  }
1170
+ if (ns && !opts?.gateLocked) {
1171
+ const key = pullGateKeyForClient(client, 'message.v2.pull', {
1172
+ after_seq: afterSeq, force: opts?.force === true, limit,
1173
+ }, `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
1174
+ return await client._runPullSerialized(key, () => this.pullV2(afterSeq, limit, {
1175
+ ...(opts ?? {}),
1176
+ gateLocked: true,
1177
+ }), true);
1178
+ }
1059
1179
  const decrypted = [];
1060
1180
  let nextAfterSeq = opts?.force ? afterSeq : (afterSeq || (ns ? client._seqTracker.getContiguousSeq(ns) : 0));
1181
+ let pullGateKey = ns
1182
+ ? pullGateKeyForClient(client, 'message.v2.pull', {
1183
+ after_seq: afterSeq, force: opts?.force === true, limit,
1184
+ }, `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`)
1185
+ : '';
1186
+ const deferredServerCursor = ns ? this.pendingForwardCursor(ns, client._seqTracker.getContiguousSeq(ns)) : 0;
1187
+ const deferredForwardAck = ns ? this.pendingForwardAck(ns, client._seqTracker.getContiguousSeq(ns)) : 0;
1061
1188
  let pageCount = 0;
1062
1189
  let lastAutoAckSeq = 0;
1063
- let pendingAckSeq = 0;
1190
+ let pendingAckSeq = deferredForwardAck;
1064
1191
  const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
1065
1192
  while (pageCount < maxPages) {
1066
1193
  pageCount += 1;
@@ -1075,24 +1202,29 @@ export class V2E2EECoordinator {
1075
1202
  ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
1076
1203
  });
1077
1204
  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) {
1205
+ const actualAckSeq = client._delivery?.resolveP2PPullAckSeq?.(result, ackUpToSeq) ?? 0;
1206
+ if (actualAckSeq >= ackUpToSeq) {
1207
+ if (pendingInlineGeneration !== null) {
1087
1208
  client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
1088
1209
  }
1210
+ pendingAckSeq = 0;
1211
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1212
+ this.clearForwardAck(ns, ackUpToSeq);
1213
+ }
1214
+ else {
1215
+ client._clientLog.warn(`message.v2.pull piggyback ACK below target: requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
1216
+ await this.confirmForwardP2PAck(ackUpToSeq, pendingInlineGeneration ?? undefined);
1217
+ pendingAckSeq = 0;
1218
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1089
1219
  }
1090
- pendingAckSeq = 0;
1091
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1092
1220
  }
1093
1221
  const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1094
1222
  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 ?? '')}`);
1223
+ const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1224
+ const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
1225
+ const hasMore = result.has_more === true;
1226
+ const serverHead = safeNonNegativeSeq(cursor?.latest_seq) ?? 0;
1227
+ 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
1228
  const seqs = messages
1097
1229
  .map((msg) => Number(msg.seq ?? 0))
1098
1230
  .filter((seq) => Number.isFinite(seq) && seq > 0);
@@ -1101,6 +1233,8 @@ export class V2E2EECoordinator {
1101
1233
  ? client._seqTracker.snapshotNamespace(ns)
1102
1234
  : null;
1103
1235
  const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
1236
+ const deferredKeyFetches = new Map();
1237
+ let blockedSeq = 0;
1104
1238
  for (const msg of messages) {
1105
1239
  const seq = Number(msg.seq ?? 0);
1106
1240
  if (!Number.isFinite(seq) || seq <= 0)
@@ -1127,7 +1261,7 @@ export class V2E2EECoordinator {
1127
1261
  attachGatewayProximity(v1Msg, msg);
1128
1262
  const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
1129
1263
  if (ns)
1130
- await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload);
1264
+ await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
1131
1265
  else
1132
1266
  await client._publishAppEvent(appEvent.event, appEvent.payload);
1133
1267
  decrypted.push(v1Msg);
@@ -1145,11 +1279,23 @@ export class V2E2EECoordinator {
1145
1279
  if (msgSpkId && client._v2Session && !client._v2Session.isCurrentSPK(msgSpkId)) {
1146
1280
  client._v2Session.trackOldSPKMaxSeq(msgSpkId, seq);
1147
1281
  }
1148
- const plaintext = await client._decryptV2Message(msg);
1282
+ const deferStatus = {};
1283
+ const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true);
1284
+ if (deferStatus.deferred) {
1285
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1286
+ }
1287
+ if (deferStatus.deferred && deferStatus.fromAid) {
1288
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1289
+ deferredKeyFetches.set(key, {
1290
+ fromAid: deferStatus.fromAid,
1291
+ senderDeviceId: deferStatus.senderDeviceId ?? '',
1292
+ groupId: deferStatus.groupId ?? '',
1293
+ });
1294
+ }
1149
1295
  if (plaintext === null)
1150
1296
  continue;
1151
1297
  if (ns) {
1152
- await client._publishPulledMessage('message.received', ns, seq, plaintext);
1298
+ await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1153
1299
  decrypted.push(plaintext);
1154
1300
  }
1155
1301
  else {
@@ -1158,10 +1304,11 @@ export class V2E2EECoordinator {
1158
1304
  }
1159
1305
  }
1160
1306
  const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
1161
- const serverAckSeq = Number(result.server_ack_seq ?? 0);
1307
+ const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
1308
+ const serverAckSeq = parsedServerAckSeq ?? 0;
1162
1309
  const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
1163
1310
  if (ns) {
1164
- const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
1311
+ const commitTarget = blockedSeq > 0 ? pageContigBefore : Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
1165
1312
  let ackSeq = pageContigBefore;
1166
1313
  let contigAdvanced = false;
1167
1314
  try {
@@ -1170,6 +1317,9 @@ export class V2E2EECoordinator {
1170
1317
  if (retentionFloor > pageContigBefore) {
1171
1318
  client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${pageContigBefore} -> retention_floor_seq=${retentionFloor}`);
1172
1319
  }
1320
+ if (deferredServerCursor > pageContigBefore && pageCount === 1) {
1321
+ client._clientLog.info(`message.v2.pull restore local A from deferred server ACK cursor: ns=${ns}, contiguous=${pageContigBefore} -> server_ack_seq=${deferredServerCursor}`);
1322
+ }
1173
1323
  if (seqs.length > 0) {
1174
1324
  client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
1175
1325
  }
@@ -1177,10 +1327,9 @@ export class V2E2EECoordinator {
1177
1327
  ackSeq = client._seqTracker.getContiguousSeq(ns);
1178
1328
  contigAdvanced = ackSeq !== pageContigBefore;
1179
1329
  if (contigAdvanced) {
1180
- await client._drainOrderedMessages(ns);
1181
- ackSeq = client._seqTracker.getContiguousSeq(ns);
1182
- contigAdvanced = ackSeq !== pageContigBefore;
1183
1330
  await client._commitSeqTrackerState(ns);
1331
+ if (pageCount === 1 && deferredServerCursor > 0)
1332
+ this.clearForwardCursor(ns, ackSeq);
1184
1333
  }
1185
1334
  }
1186
1335
  catch (exc) {
@@ -1190,11 +1339,26 @@ export class V2E2EECoordinator {
1190
1339
  client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
1191
1340
  throw exc;
1192
1341
  }
1193
- const ackNeeded = pageMessages.length > 0
1342
+ if (contigAdvanced)
1343
+ await client._drainOrderedMessages(ns, undefined, false, false);
1344
+ for (const fetch of deferredKeyFetches.values()) {
1345
+ this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1346
+ }
1347
+ if (parsedServerAckSeq !== null && serverAckSeq > ackSeq) {
1348
+ this.recordForwardCursor(ns, serverAckSeq, ackSeq);
1349
+ }
1350
+ // 无本页服务端水位时,只有本页成功提交了比已知服务端更高的 A
1351
+ // 才新增 ACK 义务。空页不能仅因本地已有 A 而重复 ACK。
1352
+ const knownServerAckSeq = hasServerAckSeq
1353
+ ? serverAckSeq
1354
+ : (pageCount === 1 ? deferredServerCursor : 0);
1355
+ const ackNeeded = blockedSeq <= 0
1194
1356
  && ackSeq > 0
1195
1357
  && ackSeq > lastAutoAckSeq
1196
- && (contigAdvanced || (hasServerAckSeq && ackSeq > serverAckSeq));
1358
+ && ((hasServerAckSeq && ackSeq > serverAckSeq)
1359
+ || (contigAdvanced && ackSeq > knownServerAckSeq));
1197
1360
  if (ackNeeded) {
1361
+ this.recordForwardAck(ns, ackSeq);
1198
1362
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
1199
1363
  const canContinuePage = messages.length > 0 && nextAfter > nextAfterSeq;
1200
1364
  if (canContinuePage) {
@@ -1202,18 +1366,34 @@ export class V2E2EECoordinator {
1202
1366
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1203
1367
  }
1204
1368
  else {
1205
- await this.ackV2(ackSeq);
1369
+ await this.confirmForwardP2PAck(ackSeq);
1206
1370
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1207
1371
  }
1208
1372
  }
1209
1373
  }
1210
1374
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
1211
1375
  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);
1376
+ if (blockedSeq > 0)
1377
+ break;
1378
+ const shouldContinue = shouldContinueForwardPage({
1379
+ rawCount,
1380
+ nextAfterSeq,
1381
+ nextAfter,
1382
+ limit,
1383
+ hasMoreField,
1384
+ hasMore,
1385
+ serverHead,
1386
+ pendingAckSeq,
1387
+ });
1215
1388
  if (!shouldContinue)
1216
1389
  break;
1390
+ if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
1391
+ const nextKey = pullGateKeyForClient(client, 'message.v2.pull', {
1392
+ after_seq: nextAfter, force: opts?.force === true, limit,
1393
+ }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
1394
+ await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
1395
+ pullGateKey = nextKey;
1396
+ }
1217
1397
  const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
1218
1398
  if (tailDelayMs > 0)
1219
1399
  await v2Sleep(tailDelayMs);
@@ -1222,13 +1402,34 @@ export class V2E2EECoordinator {
1222
1402
  if (pageCount >= maxPages) {
1223
1403
  client._clientLog.warn(`message.v2.pull reached max_pages=${maxPages} after_seq=${nextAfterSeq}`);
1224
1404
  if (pendingAckSeq > 0) {
1225
- await this.ackV2(pendingAckSeq);
1405
+ await this.confirmForwardP2PAck(pendingAckSeq);
1226
1406
  pendingAckSeq = 0;
1227
1407
  }
1228
1408
  }
1229
1409
  return decrypted;
1230
1410
  }
1411
+ async confirmForwardP2PAck(upToSeq, inlineGeneration) {
1412
+ const ns = this.client._aid ? `p2p:${this.client._aid}` : '';
1413
+ try {
1414
+ const result = await this.ackV2ForForward(upToSeq, inlineGeneration);
1415
+ const actualAckSeq = this.client._delivery?.resolveForwardAckSeq?.(result, upToSeq) ?? 0;
1416
+ if (actualAckSeq < upToSeq) {
1417
+ throw new E2EEError(`message.v2.ack server ACK watermark ${actualAckSeq} is below requested ${upToSeq}`);
1418
+ }
1419
+ this.clearForwardAck(ns, upToSeq);
1420
+ }
1421
+ catch (exc) {
1422
+ this.recordForwardAck(ns, upToSeq);
1423
+ throw exc;
1424
+ }
1425
+ }
1231
1426
  async ackV2(upToSeq, inlineGeneration) {
1427
+ return this.ackV2WithConfirmation(upToSeq, inlineGeneration, false);
1428
+ }
1429
+ async ackV2ForForward(upToSeq, inlineGeneration) {
1430
+ return this.ackV2WithConfirmation(upToSeq, inlineGeneration, true);
1431
+ }
1432
+ async ackV2WithConfirmation(upToSeq, inlineGeneration, requireServerWatermark) {
1232
1433
  const client = this.client;
1233
1434
  const ns = client._aid ? `p2p:${client._aid}` : '';
1234
1435
  let seq = upToSeq ?? (ns ? client._seqTracker.getContiguousSeq(ns) : 0);
@@ -1237,23 +1438,33 @@ export class V2E2EECoordinator {
1237
1438
  seq = client._clampAckSeq('message.v2.ack', 'up_to_seq', ns, seq);
1238
1439
  if (!Number.isSafeInteger(seq) || seq <= 0)
1239
1440
  return { acked: 0 };
1441
+ // ACK 发出前绑定当时的 inline pending;迟到响应不得结算重置后新建的 pending。
1442
+ const ackGeneration = inlineGeneration ?? client._delivery?.pendingP2PInlineAckGeneration?.(ns) ?? null;
1443
+ const v2SessionAtStart = client._v2Session;
1444
+ const v2RuntimeGenerationAtStart = Number(client._v2RuntimeGeneration ?? 0);
1240
1445
  const raw = await client._callRawV2Rpc('message.v2.ack', { up_to_seq: seq });
1241
1446
  const result = isJsonObject(raw)
1242
1447
  ? { ...raw }
1243
1448
  : { result: raw };
1244
- const actualAckSeq = client._delivery?.resolveActualAckSeq?.(result, seq) ?? seq;
1449
+ const actualAckSeq = requireServerWatermark
1450
+ ? (client._delivery?.resolveForwardAckSeq?.(result, seq) ?? 0)
1451
+ : (client._delivery?.resolveActualAckSeq?.(result, seq) ?? seq);
1452
+ if (requireServerWatermark && actualAckSeq < seq) {
1453
+ throw new E2EEError(`message.v2.ack server ACK watermark ${actualAckSeq} is below requested ${seq}`);
1454
+ }
1245
1455
  result.ack_seq = actualAckSeq;
1246
1456
  result.success = true;
1247
- if (inlineGeneration === undefined) {
1248
- client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq);
1249
- }
1250
- else {
1251
- client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, inlineGeneration);
1457
+ if (ackGeneration !== null) {
1458
+ client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, ackGeneration);
1252
1459
  }
1253
- if (client._v2Session) {
1460
+ if (v2SessionAtStart
1461
+ && client._v2Session === v2SessionAtStart
1462
+ && Number(client._v2RuntimeGeneration ?? 0) === v2RuntimeGenerationAtStart) {
1254
1463
  try {
1255
- const destroyed = await client._v2Session.maybeDestroyOldSPKs(actualAckSeq);
1256
- if (destroyed.length > 0) {
1464
+ const destroyed = await v2SessionAtStart.maybeDestroyOldSPKs(actualAckSeq);
1465
+ if (client._v2Session === v2SessionAtStart
1466
+ && Number(client._v2RuntimeGeneration ?? 0) === v2RuntimeGenerationAtStart
1467
+ && destroyed.length > 0) {
1257
1468
  client._clientLog.info(`V2 destroyed old SPKs after ack: ${destroyed.slice(0, 3)} (PFS)`);
1258
1469
  }
1259
1470
  }
@@ -1334,7 +1545,7 @@ export class V2E2EECoordinator {
1334
1545
  throw exc;
1335
1546
  }
1336
1547
  }
1337
- async pullGroupV2TailInternal(params) {
1548
+ async pullGroupV2TailInternal(params, deferPublish = false) {
1338
1549
  const client = this.client;
1339
1550
  if (params.window_mode !== 'tail')
1340
1551
  throw new ValidationError('window_mode must equal tail');
@@ -1356,7 +1567,7 @@ export class V2E2EECoordinator {
1356
1567
  const floor = Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0);
1357
1568
  const previous = client._seqTracker.getSyncState(ns);
1358
1569
  await this.applyTailFloor(ns, Number.isSafeInteger(floor) ? floor : 0);
1359
- const page = validateTailPage(result, limit);
1570
+ const page = validateTailPage(result, afterSeq, limit);
1360
1571
  client._seqTracker.commitTailWindow(ns, {
1361
1572
  windowStartSeq: page.windowStart, headSeq: page.head, coveredThroughSeq: page.head, pageLimit: limit,
1362
1573
  });
@@ -1364,7 +1575,8 @@ export class V2E2EECoordinator {
1364
1575
  await client._saveSeqTrackerState?.();
1365
1576
  const decoded = [];
1366
1577
  for (const msg of page.messages) {
1367
- const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, true, false, 'tail');
1578
+ client._logMessageDebug?.('tail-raw', 'group.v2.pull', 'group.message_created', msg);
1579
+ const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, true, false, 'tail', deferPublish);
1368
1580
  if (plaintext)
1369
1581
  decoded.push(plaintext);
1370
1582
  }
@@ -1411,22 +1623,52 @@ export class V2E2EECoordinator {
1411
1623
  let groupAid = String(opts?.groupAid ?? '').trim();
1412
1624
  const ns = `group:${gid}`;
1413
1625
  if (opts?.windowMode === 'tail') {
1626
+ if (!opts.gateLocked) {
1627
+ const key = pullGateKeyForClient(client, 'group.v2.pull', {
1628
+ group_id: gid, window_mode: 'tail', after_seq: afterSeq, limit,
1629
+ _group_cursor_params: opts?.cursorParams,
1630
+ }, `${ns}|tail|after=${afterSeq}|limit=${limit}`);
1631
+ return await client._runPullSerialized(key, () => this.pullGroupV2(gid, afterSeq, limit, {
1632
+ ...(opts ?? {}),
1633
+ gateLocked: true,
1634
+ }), false);
1635
+ }
1414
1636
  const result = await this.pullGroupV2TailInternal({
1415
1637
  group_id: String(opts.wireGroupId ?? gid), group_aid: groupAid || undefined,
1416
1638
  window_mode: 'tail', after_seq: afterSeq, limit,
1417
1639
  });
1418
1640
  return Array.isArray(result.messages) ? result.messages : [];
1419
1641
  }
1642
+ if (!opts?.gateLocked) {
1643
+ const key = pullGateKeyForClient(client, 'group.v2.pull', {
1644
+ group_id: gid, after_seq: afterSeq, force: opts?.force === true, limit,
1645
+ _group_cursor_params: opts?.cursorParams,
1646
+ }, `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
1647
+ return await client._runPullSerialized(key, () => this.pullGroupV2(gid, afterSeq, limit, {
1648
+ ...(opts ?? {}),
1649
+ gateLocked: true,
1650
+ }), true);
1651
+ }
1420
1652
  const decrypted = [];
1421
1653
  const wireGroupId = String(opts?.wireGroupId ?? groupId ?? '').trim() || gid;
1422
1654
  const cursorParams = opts?.cursorParams ?? {};
1423
1655
  const ownsCursor = opts?.ownsCursor !== false;
1656
+ let pullGateKey = ownsCursor
1657
+ ? pullGateKeyForClient(client, 'group.v2.pull', {
1658
+ group_id: gid, after_seq: afterSeq, force: opts?.force === true, limit,
1659
+ _group_cursor_params: cursorParams,
1660
+ }, `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`)
1661
+ : '';
1424
1662
  let nextAfterSeq = (opts?.explicitAfterSeq || opts?.force)
1425
1663
  ? afterSeq
1426
1664
  : (afterSeq || client._seqTracker.getContiguousSeq(ns));
1665
+ const deferredServerCursor = ownsCursor
1666
+ ? this.pendingForwardCursor(ns, client._seqTracker.getContiguousSeq(ns))
1667
+ : 0;
1668
+ const deferredForwardAck = ownsCursor ? this.pendingForwardAck(ns, client._seqTracker.getContiguousSeq(ns)) : 0;
1427
1669
  let pageCount = 0;
1428
1670
  let lastAutoAckSeq = 0;
1429
- let pendingAckSeq = 0;
1671
+ let pendingAckSeq = deferredForwardAck;
1430
1672
  const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
1431
1673
  while (pageCount < maxPages) {
1432
1674
  pageCount += 1;
@@ -1443,23 +1685,21 @@ export class V2E2EECoordinator {
1443
1685
  ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
1444
1686
  }, groupAid));
1445
1687
  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) {
1688
+ const actualAckSeq = client._delivery?.resolveGroupPullAckSeq?.(result, ackUpToSeq) ?? 0;
1689
+ if (actualAckSeq >= ackUpToSeq) {
1690
+ if (pendingInlineGeneration !== null) {
1458
1691
  client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
1459
1692
  }
1693
+ pendingAckSeq = 0;
1694
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1695
+ this.clearForwardAck(ns, ackUpToSeq);
1696
+ }
1697
+ else {
1698
+ client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
1699
+ await this.confirmForwardGroupAck(gid, ackUpToSeq, groupAid);
1700
+ pendingAckSeq = 0;
1701
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1460
1702
  }
1461
- pendingAckSeq = 0;
1462
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1463
1703
  }
1464
1704
  const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
1465
1705
  if (resultGroupAid)
@@ -1467,6 +1707,10 @@ export class V2E2EECoordinator {
1467
1707
  const eventGroupAid = resultGroupAid || groupAid || gid;
1468
1708
  const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1469
1709
  const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
1710
+ const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1711
+ const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
1712
+ const hasMore = result.has_more === true;
1713
+ const serverHead = Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0);
1470
1714
  const seqs = messages
1471
1715
  .map((msg) => Number(msg.seq ?? 0))
1472
1716
  .filter((seq) => Number.isFinite(seq) && seq > 0);
@@ -1475,6 +1719,8 @@ export class V2E2EECoordinator {
1475
1719
  ? client._seqTracker.snapshotNamespace(ns)
1476
1720
  : null;
1477
1721
  const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
1722
+ const deferredKeyFetches = new Map();
1723
+ let blockedSeq = 0;
1478
1724
  for (const msg of messages) {
1479
1725
  const seq = Number(msg.seq ?? 0);
1480
1726
  if (!Number.isFinite(seq) || seq <= 0)
@@ -1510,7 +1756,7 @@ export class V2E2EECoordinator {
1510
1756
  };
1511
1757
  v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
1512
1758
  attachGatewayProximity(v1Msg, msg);
1513
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg);
1759
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
1514
1760
  decrypted.push(v1Msg);
1515
1761
  continue;
1516
1762
  }
@@ -1529,7 +1775,7 @@ export class V2E2EECoordinator {
1529
1775
  };
1530
1776
  v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
1531
1777
  attachGatewayProximity(v1Msg, msg);
1532
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg);
1778
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
1533
1779
  decrypted.push(v1Msg);
1534
1780
  continue;
1535
1781
  }
@@ -1540,56 +1786,87 @@ export class V2E2EECoordinator {
1540
1786
  client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
1541
1787
  continue;
1542
1788
  }
1543
- let plaintext = await client._decryptV2Message(msg);
1789
+ const deferStatus = {};
1790
+ let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true);
1791
+ if (deferStatus.deferred) {
1792
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1793
+ }
1794
+ if (deferStatus.deferred && deferStatus.fromAid) {
1795
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1796
+ deferredKeyFetches.set(key, {
1797
+ fromAid: deferStatus.fromAid,
1798
+ senderDeviceId: deferStatus.senderDeviceId ?? '',
1799
+ groupId: deferStatus.groupId ?? '',
1800
+ });
1801
+ }
1544
1802
  if (plaintext === null)
1545
1803
  continue;
1546
1804
  plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
1547
1805
  plaintext.group_id = gid;
1548
1806
  plaintext.group_aid = eventGroupAid;
1549
- await client._publishPulledMessage('group.message_created', ns, seq, plaintext);
1807
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
1550
1808
  decrypted.push(plaintext);
1551
1809
  }
1552
- const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1553
- const cursorCurrentSeq = Number(cursor?.current_seq ?? 0);
1810
+ const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
1811
+ const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
1812
+ const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
1554
1813
  const retentionFloor = Math.max(Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0), Number(cursor?.retention_floor_seq ?? 0));
1555
1814
  const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
1556
1815
  const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
1557
- const commitTarget = Math.max(pageContigBefore, Number.isFinite(effectiveFloor) ? effectiveFloor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
1816
+ const commitTarget = blockedSeq > 0 ? pageContigBefore : Math.max(pageContigBefore, Number.isFinite(effectiveFloor) ? effectiveFloor : 0, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
1558
1817
  let ackSeq = pageContigBefore;
1559
1818
  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}`);
1819
+ if (ownsCursor) {
1820
+ try {
1821
+ if (commitTarget > pageContigBefore) {
1822
+ client._seqTracker.forceContiguousSeq(ns, commitTarget);
1823
+ if (effectiveFloor > pageContigBefore) {
1824
+ client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
1825
+ }
1826
+ if (deferredServerCursor > pageContigBefore && pageCount === 1) {
1827
+ client._clientLog.info(`group.v2.pull restore local A from deferred server ACK cursor: group=${gid}, ns=${ns}, contiguous=${pageContigBefore} -> current_seq=${deferredServerCursor}`);
1828
+ }
1829
+ if (seqs.length > 0) {
1830
+ client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
1831
+ }
1568
1832
  }
1569
- }
1570
- ackSeq = client._seqTracker.getContiguousSeq(ns);
1571
- contigAdvanced = ackSeq !== pageContigBefore;
1572
- if (contigAdvanced) {
1573
- await client._drainOrderedMessages(ns);
1574
1833
  ackSeq = client._seqTracker.getContiguousSeq(ns);
1575
1834
  contigAdvanced = ackSeq !== pageContigBefore;
1576
- await client._commitSeqTrackerState(ns);
1835
+ if (contigAdvanced) {
1836
+ await client._commitSeqTrackerState(ns);
1837
+ if (pageCount === 1 && deferredServerCursor > 0)
1838
+ this.clearForwardCursor(ns, ackSeq);
1839
+ }
1577
1840
  }
1578
- }
1579
- catch (exc) {
1580
- if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
1581
- client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
1841
+ catch (exc) {
1842
+ if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
1843
+ client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
1844
+ }
1845
+ client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
1846
+ throw exc;
1582
1847
  }
1583
- client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
1584
- throw exc;
1848
+ if (contigAdvanced)
1849
+ await client._drainOrderedMessages(ns, undefined, false, false);
1585
1850
  }
1586
- const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
1587
- const ackNeeded = pageMessages.length > 0
1851
+ for (const fetch of deferredKeyFetches.values()) {
1852
+ this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1853
+ }
1854
+ if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
1855
+ this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
1856
+ }
1857
+ // 无本页 cursor.current_seq 时,只有本页成功提交了比已知 cursor
1858
+ // 更高的 A 才新增 ACK 义务;空页不得因已有本地 A 重复确认。
1859
+ const knownServerCursorSeq = hasServerCursor
1860
+ ? cursorCurrentSeq
1861
+ : (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
1862
+ const ackNeeded = blockedSeq <= 0
1588
1863
  && ackSeq > 0
1589
1864
  && ackSeq > lastAutoAckSeq
1590
1865
  && ownsCursor
1591
- && (contigAdvanced || (hasServerCursor && ackSeq > cursorCurrentSeq));
1866
+ && ((hasServerCursor && ackSeq > cursorCurrentSeq)
1867
+ || (contigAdvanced && ackSeq > knownServerCursorSeq));
1592
1868
  if (ackNeeded) {
1869
+ this.recordForwardAck(ns, ackSeq);
1593
1870
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
1594
1871
  const canContinuePage = messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
1595
1872
  if (canContinuePage) {
@@ -1597,7 +1874,7 @@ export class V2E2EECoordinator {
1597
1874
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1598
1875
  }
1599
1876
  else {
1600
- await this.ackGroupV2(gid, ackSeq);
1877
+ await this.confirmForwardGroupAck(gid, ackSeq, groupAid);
1601
1878
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1602
1879
  }
1603
1880
  }
@@ -1605,11 +1882,28 @@ export class V2E2EECoordinator {
1605
1882
  if (!ownsCursor)
1606
1883
  break;
1607
1884
  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);
1885
+ if (blockedSeq > 0)
1886
+ break;
1887
+ const shouldContinue = shouldContinueForwardPage({
1888
+ rawCount,
1889
+ nextAfterSeq,
1890
+ nextAfter,
1891
+ limit,
1892
+ hasMoreField,
1893
+ hasMore,
1894
+ serverHead,
1895
+ pendingAckSeq,
1896
+ });
1611
1897
  if (!shouldContinue)
1612
1898
  break;
1899
+ if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
1900
+ const nextKey = pullGateKeyForClient(client, 'group.v2.pull', {
1901
+ group_id: gid, after_seq: nextAfter, force: opts?.force === true, limit,
1902
+ _group_cursor_params: cursorParams,
1903
+ }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
1904
+ await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
1905
+ pullGateKey = nextKey;
1906
+ }
1613
1907
  const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
1614
1908
  if (tailDelayMs > 0)
1615
1909
  await v2Sleep(tailDelayMs);
@@ -1618,13 +1912,34 @@ export class V2E2EECoordinator {
1618
1912
  if (pageCount >= maxPages) {
1619
1913
  client._clientLog.warn(`group.v2.pull reached max_pages=${maxPages} group=${gid} after_seq=${nextAfterSeq}`);
1620
1914
  if (pendingAckSeq > 0) {
1621
- await this.ackGroupV2(gid, pendingAckSeq);
1915
+ await this.confirmForwardGroupAck(gid, pendingAckSeq, groupAid);
1622
1916
  pendingAckSeq = 0;
1623
1917
  }
1624
1918
  }
1625
1919
  return decrypted;
1626
1920
  }
1921
+ async confirmForwardGroupAck(groupId, upToSeq, groupAid) {
1922
+ const ns = `group:${String(groupId ?? '').trim()}`;
1923
+ try {
1924
+ const result = await this.ackGroupV2ForForward(groupId, upToSeq, groupAid);
1925
+ const actualAckSeq = this.client._delivery?.resolveForwardAckSeq?.(result, upToSeq) ?? 0;
1926
+ if (actualAckSeq < upToSeq) {
1927
+ throw new E2EEError(`group.v2.ack server ACK watermark ${actualAckSeq} is below requested ${upToSeq}`);
1928
+ }
1929
+ this.clearForwardAck(ns, upToSeq);
1930
+ }
1931
+ catch (exc) {
1932
+ this.recordForwardAck(ns, upToSeq);
1933
+ throw exc;
1934
+ }
1935
+ }
1627
1936
  async ackGroupV2(groupId, upToSeq, groupAid, inlineGeneration) {
1937
+ return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, inlineGeneration, false);
1938
+ }
1939
+ async ackGroupV2ForForward(groupId, upToSeq, groupAid) {
1940
+ return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, undefined, true);
1941
+ }
1942
+ async ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, inlineGeneration, requireServerWatermark) {
1628
1943
  const client = this.client;
1629
1944
  const gid = typeof groupId === 'string' ? groupId.trim() : '';
1630
1945
  if (!gid)
@@ -1644,13 +1959,17 @@ export class V2E2EECoordinator {
1644
1959
  seq = client._clampAckSeq('group.v2.ack', 'up_to_seq', ns, seq);
1645
1960
  if (!Number.isSafeInteger(seq) || seq <= 0)
1646
1961
  return { acked: 0 };
1962
+ // 与 P2P 一致,必须将 ACK 响应限定到请求开始时对应的 inline generation。
1963
+ const ackGeneration = inlineGeneration ?? client._delivery?.pendingGroupInlineAckGeneration?.(ns) ?? null;
1647
1964
  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);
1965
+ const actualAckSeq = requireServerWatermark
1966
+ ? (client._delivery?.resolveForwardAckSeq?.(raw, seq) ?? 0)
1967
+ : (client._delivery?.resolveActualAckSeq?.(raw, seq) ?? seq);
1968
+ if (requireServerWatermark && actualAckSeq < seq) {
1969
+ throw new E2EEError(`group.v2.ack server ACK watermark ${actualAckSeq} is below requested ${seq}`);
1651
1970
  }
1652
- else {
1653
- client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq, inlineGeneration);
1971
+ if (ackGeneration !== null) {
1972
+ client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq, ackGeneration);
1654
1973
  }
1655
1974
  return raw;
1656
1975
  }
@@ -1830,7 +2149,7 @@ export class V2E2EECoordinator {
1830
2149
  let e2ee;
1831
2150
  if (payload?.type === 'e2ee.group_encrypted' && String(payload.version ?? '') === 'v2') {
1832
2151
  e2ee = v2E2eeMeta(payload);
1833
- const plain = await client._decryptV2EnvelopeForThought({ envelope: payload, fromAid });
2152
+ const plain = await client._decryptV2EnvelopeForThought({ envelope: payload, fromAid, fetchSenderIK: true });
1834
2153
  if (plain === null) {
1835
2154
  decryptFailed = true;
1836
2155
  client._clientLog.debug(`group.thought.get decrypt returned null: group=${groupId}, thought_id=${thoughtId}, from=${fromAid}`);
@@ -1900,7 +2219,7 @@ export class V2E2EECoordinator {
1900
2219
  let e2ee;
1901
2220
  if (payload?.type === 'e2ee.p2p_encrypted' && String(payload.version ?? '') === 'v2') {
1902
2221
  e2ee = v2E2eeMeta(payload);
1903
- const plain = await client._decryptV2EnvelopeForThought({ envelope: payload, fromAid });
2222
+ const plain = await client._decryptV2EnvelopeForThought({ envelope: payload, fromAid, fetchSenderIK: true });
1904
2223
  if (plain === null) {
1905
2224
  decryptFailed = true;
1906
2225
  client._clientLog.debug(`message.thought.get decrypt returned null: thought_id=${thoughtId}, from=${fromAid}, to=${toAid}`);
@@ -1987,7 +2306,10 @@ export class V2E2EECoordinator {
1987
2306
  const fromAid = String(opts.fromAid || aad.from || '').trim();
1988
2307
  const senderDeviceId = String(aad.from_device ?? '');
1989
2308
  const senderCertFingerprint = String(envelope.sender_cert_fingerprint ?? '').trim().toLowerCase();
1990
- const senderPubDer = await this.getV2SenderPubDer(fromAid, senderDeviceId, senderCertFingerprint);
2309
+ let senderPubDer = await this.getV2SenderPubDer(fromAid, senderDeviceId, senderCertFingerprint);
2310
+ if (!senderPubDer && opts.fetchSenderIK) {
2311
+ senderPubDer = await this.fetchSenderIKOnce(fromAid, senderDeviceId, groupIdForKeys, senderCertFingerprint);
2312
+ }
1991
2313
  if (!senderPubDer) {
1992
2314
  client._clientLog.warn(`V2 thought decrypt: no sender IK for ${fromAid} device=${senderDeviceId}`);
1993
2315
  this.scheduleSenderIKFetch(fromAid, senderDeviceId, groupIdForKeys);