@agentunion/fastaun 0.5.4 → 0.5.6

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 (133) hide show
  1. package/CHANGELOG.md +126 -90
  2. package/_packed_docs/AUN/347/246/273/347/272/277/346/216/250/351/200/201/346/216/245/345/205/245/344/270/216/346/274/224/347/244/272/346/214/207/345/215/227.md +290 -0
  3. package/_packed_docs/AUN/347/246/273/347/272/277/346/216/250/351/200/201/346/234/215/345/212/241/346/236/266/346/236/204/344/270/216/350/257/246/347/273/206/345/256/236/347/216/260/350/256/241/345/210/222-codex.md +994 -0
  4. package/_packed_docs/AUN/347/246/273/347/272/277/346/216/250/351/200/201/346/234/215/345/212/241/350/277/220/347/273/264/344/270/216/345/217/221/345/270/203/346/214/207/345/215/227.md +144 -0
  5. package/_packed_docs/CHANGELOG.md +126 -90
  6. package/_packed_docs/INDEX.md +127 -69
  7. package/_packed_docs/KITE_DOCS_GUIDE.md +63 -29
  8. 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 +366 -0
  9. 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 +227 -0
  10. 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 +1004 -0
  11. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +999 -0
  12. package/_packed_docs/cli/CLI/346/211/213/345/206/214.md +6 -4
  13. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +58 -20
  14. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +219 -247
  15. package/_packed_docs/protocol/12-Stream-/345/255/220/345/215/217/350/256/256.md +14 -14
  16. package/_packed_docs/protocol/13-Agent/350/241/214/344/270/272/350/247/204/350/214/203.md +3 -3
  17. package/_packed_docs/protocol/15-/347/246/273/347/272/277/346/216/250/351/200/201/351/200/232/347/237/245/345/215/217/350/256/256.md +165 -421
  18. package/_packed_docs/protocol/README.md +1 -0
  19. package/_packed_docs/protocol/aun-docs-guide.md +7 -4
  20. package/_packed_docs/protocol/index.md +25 -19
  21. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +62 -17
  22. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +22 -0
  23. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +67 -78
  24. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +6 -2
  25. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +50 -14
  26. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +18 -5
  27. package/_packed_docs/sdk/09-group-rpc-manual.md +312 -282
  28. package/_packed_docs/sdk/09-message-rpc-manual.md +142 -103
  29. package/_packed_docs/sdk/09-payload-reference.md +1 -1
  30. package/_packed_docs/sdk/09-stream-rpc-manual.md +8 -8
  31. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +26 -17
  32. package/_packed_docs/sdk/INDEX.md +42 -34
  33. package/_packed_docs/sdk/README.md +7 -6
  34. package/_packed_docs//346/266/210/346/201/257/345/220/214/346/255/245/344/270/216/345/216/206/345/217/262/346/213/211/345/217/226/344/273/243/347/240/201/345/256/241/346/237/245/351/227/256/351/242/230/346/270/205/345/215/225.md +229 -0
  35. package/_packed_docs//346/266/210/346/201/257/345/220/214/346/255/245/344/270/216/345/216/206/345/217/262/346/213/211/345/217/226/346/224/271/351/200/240/346/226/271/346/241/210.md +750 -0
  36. package/dist/agent-md.js +8 -2
  37. package/dist/agent-md.js.map +1 -1
  38. package/dist/aid-store.d.ts +2 -0
  39. package/dist/aid-store.js +43 -7
  40. package/dist/aid-store.js.map +1 -1
  41. package/dist/auth.d.ts +0 -2
  42. package/dist/auth.js +97 -50
  43. package/dist/auth.js.map +1 -1
  44. package/dist/client/delivery.d.ts +70 -7
  45. package/dist/client/delivery.js +1332 -236
  46. package/dist/client/delivery.js.map +1 -1
  47. package/dist/client/group-state.js +25 -22
  48. package/dist/client/group-state.js.map +1 -1
  49. package/dist/client/lifecycle.js +46 -3
  50. package/dist/client/lifecycle.js.map +1 -1
  51. package/dist/client/mention-mode.d.ts +6 -0
  52. package/dist/client/mention-mode.js +184 -0
  53. package/dist/client/mention-mode.js.map +1 -0
  54. package/dist/client/peers.d.ts +1 -1
  55. package/dist/client/peers.js +26 -3
  56. package/dist/client/peers.js.map +1 -1
  57. package/dist/client/rpc-pipeline.d.ts +20 -1
  58. package/dist/client/rpc-pipeline.js +332 -73
  59. package/dist/client/rpc-pipeline.js.map +1 -1
  60. package/dist/client/v2-e2ee.d.ts +20 -1
  61. package/dist/client/v2-e2ee.js +625 -105
  62. package/dist/client/v2-e2ee.js.map +1 -1
  63. package/dist/client.d.ts +17 -5
  64. package/dist/client.js +332 -167
  65. package/dist/client.js.map +1 -1
  66. package/dist/errors.js +4 -1
  67. package/dist/errors.js.map +1 -1
  68. package/dist/facades.d.ts +3 -0
  69. package/dist/facades.js +109 -53
  70. package/dist/facades.js.map +1 -1
  71. package/dist/group-index.d.ts +5 -0
  72. package/dist/group-index.js +65 -30
  73. package/dist/group-index.js.map +1 -1
  74. package/dist/keystore/aid-db.d.ts +12 -0
  75. package/dist/keystore/aid-db.js +68 -4
  76. package/dist/keystore/aid-db.js.map +1 -1
  77. package/dist/keystore/index.d.ts +12 -0
  78. package/dist/keystore/local-identity-store.d.ts +10 -0
  79. package/dist/keystore/local-identity-store.js +8 -2
  80. package/dist/keystore/local-identity-store.js.map +1 -1
  81. package/dist/keystore/local-token-store.d.ts +10 -0
  82. package/dist/keystore/local-token-store.js +8 -2
  83. package/dist/keystore/local-token-store.js.map +1 -1
  84. package/dist/register-flow.d.ts +0 -1
  85. package/dist/register-flow.js +96 -38
  86. package/dist/register-flow.js.map +1 -1
  87. package/dist/secret-store/file-store.d.ts +3 -14
  88. package/dist/secret-store/file-store.js +7 -98
  89. package/dist/secret-store/file-store.js.map +1 -1
  90. package/dist/secret-store/index.d.ts +1 -4
  91. package/dist/secret-store/index.js +2 -2
  92. package/dist/secret-store/index.js.map +1 -1
  93. package/dist/seq-tracker.d.ts +29 -9
  94. package/dist/seq-tracker.js +233 -64
  95. package/dist/seq-tracker.js.map +1 -1
  96. package/dist/storage/vfs.d.ts +1 -0
  97. package/dist/storage/vfs.js +28 -4
  98. package/dist/storage/vfs.js.map +1 -1
  99. package/dist/tools/cross-sdk-agent.js +360 -8
  100. package/dist/tools/cross-sdk-agent.js.map +1 -1
  101. package/dist/transport.d.ts +19 -0
  102. package/dist/transport.js +197 -14
  103. package/dist/transport.js.map +1 -1
  104. package/dist/v2/e2ee/decrypt.js +26 -23
  105. package/dist/v2/e2ee/decrypt.js.map +1 -1
  106. package/dist/v2/session/session.js +3 -4
  107. package/dist/v2/session/session.js.map +1 -1
  108. package/dist/v2/state/commitment.js +1 -2
  109. package/dist/v2/state/commitment.js.map +1 -1
  110. package/dist/version.d.ts +1 -1
  111. package/dist/version.js +1 -1
  112. package/package.json +5 -4
  113. package/dist/e2ee-group.d.ts +0 -249
  114. package/dist/e2ee-group.js +0 -1567
  115. package/dist/e2ee-group.js.map +0 -1
  116. package/dist/group-resources.d.ts +0 -31
  117. package/dist/group-resources.js +0 -84
  118. package/dist/group-resources.js.map +0 -1
  119. package/dist/keystore/file.d.ts +0 -109
  120. package/dist/keystore/file.js +0 -772
  121. package/dist/keystore/file.js.map +0 -1
  122. package/dist/keystore/sqlite-backup.d.ts +0 -44
  123. package/dist/keystore/sqlite-backup.js +0 -382
  124. package/dist/keystore/sqlite-backup.js.map +0 -1
  125. package/dist/namespaces/auth.d.ts +0 -118
  126. package/dist/namespaces/auth.js +0 -968
  127. package/dist/namespaces/auth.js.map +0 -1
  128. package/dist/namespaces/custody.d.ts +0 -48
  129. package/dist/namespaces/custody.js +0 -313
  130. package/dist/namespaces/custody.js.map +0 -1
  131. package/dist/namespaces/meta.d.ts +0 -76
  132. package/dist/namespaces/meta.js +0 -490
  133. package/dist/namespaces/meta.js.map +0 -1
@@ -1,16 +1,17 @@
1
1
  import { slotIsolationKey } from '../config.js';
2
- import { normalizeGroupId } from '../group-id.js';
3
2
  import { ConnectionState, isJsonObject } from '../types.js';
3
+ import { hasGroupMentionModeField, normalizeGroupMentionMode } from './mention-mode.js';
4
4
  const PUSHED_SEQS_LIMIT = 50_000;
5
5
  const PENDING_ORDERED_LIMIT = 50_000;
6
6
  const MESSAGE_RECALL_SEEN_LIMIT = 10_000;
7
7
  const GROUP_RECALL_SEEN_LIMIT = 10_000;
8
8
  const SEQ_TRACKER_PERSIST_FLUSH_DELAY_MS = 200;
9
+ const MAX_INLINE_PENDING_ACK_NAMESPACES = 4096;
9
10
  const APP_MESSAGE_ENVELOPE_KEYS = [
10
11
  'module_id', 'message_type', 'type', 'kind', 'version',
11
12
  'from', 'from_aid', 'sender_aid', 'to', 'to_aid', 'group_id',
12
13
  'timestamp', 'created_at', 'encrypted',
13
- 'context', 'protected_headers', 'headers', 'payload_type',
14
+ 'context', 'protected_headers', 'headers', 'payload_type', 'mention_mode',
14
15
  ];
15
16
  const RECALL_PAYLOAD_KEYS = [
16
17
  'message_ids', 'target_message_seqs',
@@ -38,9 +39,57 @@ function formatDeliveryError(error) {
38
39
  }
39
40
  export class MessageDeliveryEngine {
40
41
  runtime;
42
+ realtimeTailHeads = null;
43
+ realtimeSyncing = null;
44
+ pendingP2PInlineAcks = null;
45
+ pendingGroupInlineAcks = null;
46
+ pendingGroupInlineAckNamespaces = null;
47
+ inlineAckGeneration = 0;
41
48
  constructor(runtime) {
42
49
  this.runtime = runtime;
43
50
  }
51
+ resetInlineAckState() {
52
+ this.inlineAckGeneration += 1;
53
+ this.pendingP2PInlineAcks = null;
54
+ this.pendingGroupInlineAcks = null;
55
+ this.pendingGroupInlineAckNamespaces = null;
56
+ this.realtimeSyncing = null;
57
+ this.realtimeTailHeads = null;
58
+ }
59
+ inlineGenerationIsCurrent(generation) {
60
+ return this.inlineAckGeneration === generation;
61
+ }
62
+ captureInlineAckGeneration() {
63
+ return this.inlineAckGeneration;
64
+ }
65
+ captureP2PInlineAckGeneration() {
66
+ return this.pendingP2PInlineAcks?.size ? this.inlineAckGeneration : null;
67
+ }
68
+ captureGroupInlineAckGeneration() {
69
+ return this.pendingGroupInlineAcks?.size ? this.inlineAckGeneration : null;
70
+ }
71
+ pendingInlineAckNamespaceCount() {
72
+ return (this.pendingP2PInlineAcks?.size ?? 0) + (this.pendingGroupInlineAcks?.size ?? 0);
73
+ }
74
+ markRealtimeTailCompleted(result, tailHead) {
75
+ const heads = this.realtimeTailHeads ?? new WeakMap();
76
+ this.realtimeTailHeads = heads;
77
+ heads.set(result, tailHead);
78
+ }
79
+ realtimeTailHead(result) {
80
+ return this.realtimeTailHeads?.get(result) ?? null;
81
+ }
82
+ beginRealtimeSync(ns) {
83
+ if (!ns || this.realtimeSyncing?.has(ns))
84
+ return false;
85
+ const syncing = this.realtimeSyncing ?? new Set();
86
+ this.realtimeSyncing = syncing;
87
+ syncing.add(ns);
88
+ return true;
89
+ }
90
+ endRealtimeSync(ns) {
91
+ this.realtimeSyncing?.delete(ns);
92
+ }
44
93
  prunePushedSeqs(ns) {
45
94
  const client = this.runtime.client;
46
95
  const pushed = client._pushedSeqs.get(ns);
@@ -108,8 +157,8 @@ export class MessageDeliveryEngine {
108
157
  const eventPayload = payload;
109
158
  client._groupState?.handleGroupChangedV2Membership?.(eventPayload);
110
159
  if (eventPayload.action === 'dissolved') {
111
- const rawGroupId = eventPayload.group_aid ?? eventPayload.group_id ?? '';
112
- const groupId = normalizeGroupId(String(rawGroupId)) || String(rawGroupId).trim();
160
+ const rawGroupId = eventPayload.group_id ?? eventPayload.group_aid ?? '';
161
+ const groupId = String(rawGroupId).trim();
113
162
  if (groupId)
114
163
  client._cleanupDissolvedGroup?.(groupId);
115
164
  }
@@ -140,7 +189,10 @@ export class MessageDeliveryEngine {
140
189
  return result;
141
190
  }
142
191
  normalizePublishedMessagePayload(event, payload) {
192
+ payload = this.stripInlineInternalFields(payload);
143
193
  if (this.isInstanceScopedMessageEvent(event)) {
194
+ if (event === 'group.message_created')
195
+ payload = normalizeGroupMentionMode(payload);
144
196
  return this.attachAppMessageEnvelope(this.stripInternalSenderDeviceFields(this.attachCurrentInstanceContext(payload)));
145
197
  }
146
198
  if (this.isGroupScopedEvent(event)) {
@@ -148,6 +200,18 @@ export class MessageDeliveryEngine {
148
200
  }
149
201
  return payload;
150
202
  }
203
+ stripInlineInternalFields(payload) {
204
+ if (!isJsonObject(payload))
205
+ return payload;
206
+ const source = payload;
207
+ if (!Object.prototype.hasOwnProperty.call(source, 'inline_message')
208
+ && !Object.prototype.hasOwnProperty.call(source, '_inline_message_candidates_v1'))
209
+ return payload;
210
+ const result = { ...source };
211
+ delete result.inline_message;
212
+ delete result._inline_message_candidates_v1;
213
+ return result;
214
+ }
151
215
  envelopeMetadata(value) {
152
216
  let source = value;
153
217
  if (source && typeof source === 'object') {
@@ -205,6 +269,7 @@ export class MessageDeliveryEngine {
205
269
  if (protectedHeaders)
206
270
  envelope.protected_headers = protectedHeaders;
207
271
  setIfPresent('payload_type', firstValue(message.payload_type, protectedHeaders?.payload_type));
272
+ setIfPresent('mention_mode', message.mention_mode);
208
273
  return envelope;
209
274
  }
210
275
  isGroupScopedEvent(event) {
@@ -271,19 +336,30 @@ export class MessageDeliveryEngine {
271
336
  if (protectedHeaders)
272
337
  envelope.protected_headers = protectedHeaders;
273
338
  setIfPresent('payload_type', firstValue(params.payload_type, protectedHeaders?.payload_type, body.type));
339
+ if (method === 'group.send')
340
+ setIfPresent('mention_mode', resultObj.mention_mode);
274
341
  return envelope;
275
342
  }
276
- attachSendResultEnvelope(method, params, result, encrypted) {
343
+ attachSendResultEnvelope(method, params, result, encrypted, modeSource) {
277
344
  if (!APP_SEND_ENVELOPE_METHODS.has(method) || !isJsonObject(result))
278
345
  return result;
279
- const out = { ...result };
280
- out.envelope = this.sendResultEnvelope(method, params, out, encrypted);
346
+ let out = { ...result };
347
+ const serverEnvelope = method === 'group.send' && isJsonObject(out.envelope)
348
+ ? { ...out.envelope }
349
+ : undefined;
281
350
  if ('payload' in params) {
282
351
  out.payload = params.payload;
283
352
  }
284
353
  else if ('content' in params) {
285
354
  out.payload = params.content;
286
355
  }
356
+ const generatedEnvelope = this.sendResultEnvelope(method, params, out, encrypted);
357
+ out.envelope = serverEnvelope ? { ...generatedEnvelope, ...serverEnvelope } : generatedEnvelope;
358
+ if (method === 'group.send') {
359
+ const serverSource = modeSource ?? result;
360
+ const authoritativeSource = hasGroupMentionModeField(serverSource) ? serverSource : out;
361
+ out = normalizeGroupMentionMode(out, authoritativeSource);
362
+ }
287
363
  return out;
288
364
  }
289
365
  attachAppGroupEventEnvelope(payload) {
@@ -473,8 +549,7 @@ export class MessageDeliveryEngine {
473
549
  // (group_id, sorted message_ids) 已能唯一标识一次撤回;缺 message_ids 时按原消息 id/seq 兜底。
474
550
  // 去重键不含 recalled_at:占位/通知 tombstone(pull,事务内时间)与在线 push(事务后重取时间)
475
551
  // 三条通道对同一次撤回可能携带不同来源的时间戳,纳入 recalled_at 会使去重失效、回调多次。
476
- // group_id 归一化一次,使去重键与来源无关(pull normalizeGroupId,push 可能是原始值)。
477
- const normalizedGroupId = normalizeGroupId(groupId) || String(groupId ?? '').trim();
552
+ const normalizedGroupId = String(groupId ?? '').trim();
478
553
  const ids = payload.message_ids;
479
554
  const idPart = Array.isArray(ids)
480
555
  ? ids.map((i) => String(i ?? '').trim()).filter(Boolean).sort().join(',')
@@ -503,9 +578,7 @@ export class MessageDeliveryEngine {
503
578
  if (!eventPayload)
504
579
  return false;
505
580
  const rawGroupId = String(eventPayload.group_id ?? groupId ?? '').trim();
506
- const dedupGroupId = normalizeGroupId(rawGroupId) || rawGroupId;
507
- if (dedupGroupId)
508
- eventPayload.group_id = dedupGroupId;
581
+ const dedupGroupId = rawGroupId;
509
582
  const dedupKey = this.groupRecallDedupKey(dedupGroupId, eventPayload);
510
583
  let seen = client._groupRecallSeen;
511
584
  if (!seen) {
@@ -643,11 +716,11 @@ export class MessageDeliveryEngine {
643
716
  if (!isJsonObject(message)) {
644
717
  return { value_type: typeof message };
645
718
  }
646
- const msg = message;
719
+ const msg = normalizeGroupMentionMode(message);
647
720
  const keys = [
648
721
  'message_id', 'id', 'from', 'from_aid', 'sender_aid', 'to', 'to_aid',
649
722
  'group_id', 'seq', 'msg_seq', 'type', 'version', 'timestamp', 't_server',
650
- 'device_id', 'slot_id', 'encrypted', 'dispatch_mode', 'dispatch',
723
+ 'device_id', 'slot_id', 'encrypted', 'mention_mode', 'dispatch',
651
724
  'e2ee', 'headers', 'protected_headers', 'context', 'status',
652
725
  '_decrypt_error', '_decrypt_stage',
653
726
  ];
@@ -684,24 +757,72 @@ export class MessageDeliveryEngine {
684
757
  return true;
685
758
  const client = this.runtime.client;
686
759
  const msg = message;
687
- if ('device_id' in msg) {
688
- const targetDeviceId = String(msg.device_id ?? '').trim();
689
- if (targetDeviceId !== client._deviceId) {
760
+ if (Object.prototype.hasOwnProperty.call(msg, 'target_device_id')) {
761
+ if (typeof msg.target_device_id !== 'string')
762
+ return false;
763
+ const targetDeviceId = msg.target_device_id.trim();
764
+ if (!targetDeviceId || targetDeviceId !== client._deviceId) {
690
765
  return false;
691
766
  }
692
767
  }
693
- if ('slot_id' in msg) {
694
- const targetSlotId = String(msg.slot_id ?? '').trim();
695
- if (slotIsolationKey(targetSlotId) !== slotIsolationKey(client._slotId)) {
768
+ else if (Object.prototype.hasOwnProperty.call(msg, 'device_id')) {
769
+ if (typeof msg.device_id !== 'string')
770
+ return false;
771
+ const deviceId = msg.device_id.trim();
772
+ if (deviceId !== client._deviceId)
773
+ return false;
774
+ }
775
+ if (Object.prototype.hasOwnProperty.call(msg, 'target_slot_id')) {
776
+ if (typeof msg.target_slot_id !== 'string')
777
+ return false;
778
+ const targetSlotId = msg.target_slot_id.trim();
779
+ if (!targetSlotId || slotIsolationKey(targetSlotId) !== slotIsolationKey(client._slotId)) {
696
780
  return false;
697
781
  }
698
782
  }
783
+ else if (Object.prototype.hasOwnProperty.call(msg, 'slot_id')) {
784
+ if (typeof msg.slot_id !== 'string')
785
+ return false;
786
+ const slotId = msg.slot_id.trim();
787
+ if (slotIsolationKey(slotId) !== slotIsolationKey(client._slotId))
788
+ return false;
789
+ }
699
790
  return true;
700
791
  }
701
- async runPushProcessSerialized(ns, operation) {
792
+ async runPushProcessSerialized(ns, operation, inlineGeneration) {
702
793
  const client = this.runtime.client;
703
- if (!ns) {
794
+ if (inlineGeneration === undefined) {
795
+ if (!ns) {
796
+ await operation();
797
+ return;
798
+ }
799
+ let queues = client._pushProcessQueues;
800
+ if (!queues) {
801
+ queues = new Map();
802
+ client._pushProcessQueues = queues;
803
+ }
804
+ const previous = queues.get(ns) ?? Promise.resolve();
805
+ const current = previous.catch(() => undefined).then(operation);
806
+ const stored = current.then(() => undefined, () => undefined);
807
+ queues.set(ns, stored);
808
+ try {
809
+ await current;
810
+ }
811
+ finally {
812
+ if (queues.get(ns) === stored)
813
+ queues.delete(ns);
814
+ }
815
+ return;
816
+ }
817
+ const run = async () => {
818
+ if (!this.inlineGenerationIsCurrent(inlineGeneration))
819
+ return;
704
820
  await operation();
821
+ if (!this.inlineGenerationIsCurrent(inlineGeneration))
822
+ return;
823
+ };
824
+ if (!ns) {
825
+ await run();
705
826
  return;
706
827
  }
707
828
  let queues = client._pushProcessQueues;
@@ -710,7 +831,7 @@ export class MessageDeliveryEngine {
710
831
  client._pushProcessQueues = queues;
711
832
  }
712
833
  const previous = queues.get(ns) ?? Promise.resolve();
713
- const current = previous.catch(() => undefined).then(operation);
834
+ const current = previous.catch(() => undefined).then(run);
714
835
  const stored = current.then(() => undefined, () => undefined);
715
836
  queues.set(ns, stored);
716
837
  try {
@@ -734,7 +855,10 @@ export class MessageDeliveryEngine {
734
855
  const ns = isJsonObject(data) && client._aid && data.seq !== undefined
735
856
  ? `p2p:${client._aid}`
736
857
  : '';
737
- this.runPushProcessSerialized(ns, () => this.processAndPublishMessage(data)).catch((exc) => {
858
+ const inlineGeneration = isJsonObject(data)
859
+ && Object.prototype.hasOwnProperty.call(data, 'inline_message')
860
+ ? this.captureInlineAckGeneration() : undefined;
861
+ this.runPushProcessSerialized(ns, () => this.processAndPublishMessage(data, inlineGeneration), inlineGeneration).catch((exc) => {
738
862
  client._clientLog.warn(`P2P message decrypt failed: ${formatDeliveryError(exc)}`);
739
863
  if (isJsonObject(data)) {
740
864
  const src = data;
@@ -770,7 +894,7 @@ export class MessageDeliveryEngine {
770
894
  const contigBefore = client._seqTracker.getContiguousSeq(ns);
771
895
  const published = await this.publishOrderedMessage('message.recalled', ns, seq, msg);
772
896
  const contigAfter = client._seqTracker.getContiguousSeq(ns);
773
- if (Number(seq) > contigAfter && !published) {
897
+ if (Number(seq) > contigAfter && !published && client._sessionOptions?.background_sync !== false) {
774
898
  this.fillP2pGap().catch((exc) => client._clientLog.warn(`background recall gap fill failed: ${formatDeliveryError(exc)}`));
775
899
  }
776
900
  const contig = client._seqTracker.getContiguousSeq(ns);
@@ -785,7 +909,7 @@ export class MessageDeliveryEngine {
785
909
  }
786
910
  await this.publishMessageRecallTombstone(seq, msg);
787
911
  }
788
- async processAndPublishMessage(data) {
912
+ async processAndPublishMessage(data, inlineGeneration) {
789
913
  const client = this.runtime.client;
790
914
  if (!isJsonObject(data)) {
791
915
  await client._publishAppEvent('message.received', data, 'push');
@@ -796,6 +920,22 @@ export class MessageDeliveryEngine {
796
920
  client._clientLog.debug(`P2P push filtered by instance: message_id=${String(msg.message_id ?? '')}, seq=${String(msg.seq ?? '')}, target_device=${String(msg.device_id ?? '')}, target_slot=${String(msg.slot_id ?? '')}, local_device=${client._deviceId}, local_slot=${client._slotId}`);
797
921
  return;
798
922
  }
923
+ // 新 SDK 的所有带合法 seq P2P Push 都只作为 Head 通知处理。
924
+ // 新服务端为兼容旧 SDK 仍可能发送 message.received;V2 会话下必须
925
+ // 在旧 seq tracking / ordered publish 之前转入前台 Tail 协调器。
926
+ const inlinePresent = Object.prototype.hasOwnProperty.call(msg, 'inline_message');
927
+ const legacyPushSeq = this.realtimeHeadSeq(msg.seq, inlinePresent);
928
+ if (client._v2Session && client._aid && msg.seq !== undefined && msg.seq !== null) {
929
+ if (legacyPushSeq === null) {
930
+ if (inlinePresent)
931
+ await this.recoverInvalidP2PRealtimeHead(`p2p:${client._aid}`, msg.seq);
932
+ return;
933
+ }
934
+ await this.onV2PushNotification({ ...msg, seq: legacyPushSeq }, 'v1', inlineGeneration);
935
+ if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
936
+ return;
937
+ return;
938
+ }
799
939
  const encryptedPush = client._isEncryptedPushMessage(msg);
800
940
  const seq = msg.seq;
801
941
  if (seq !== undefined && seq !== null && client._aid) {
@@ -808,7 +948,7 @@ export class MessageDeliveryEngine {
808
948
  : await this.publishOrderedMessage('message.received', ns, seq, msg);
809
949
  const contigAfter = client._seqTracker.getContiguousSeq(ns);
810
950
  const needPull = Number(seq) > contigAfter && !published;
811
- if (needPull) {
951
+ if (needPull && client._sessionOptions?.background_sync !== false) {
812
952
  client._clientLog.debug(`P2P seq gap detected: ns=${ns}, seq=${seq}, contiguous=${contigAfter}`);
813
953
  this.fillP2pGap().catch((exc) => client._clientLog.warn(`background gap fill trigger failed: ${formatDeliveryError(exc)}`));
814
954
  }
@@ -847,7 +987,10 @@ export class MessageDeliveryEngine {
847
987
  const groupNs = isJsonObject(data)
848
988
  ? String(data.group_id ?? '').trim()
849
989
  : '';
850
- this.runPushProcessSerialized(groupNs ? `group:${groupNs}` : '', () => this.processAndPublishGroupMessage(data)).catch((exc) => {
990
+ const inlineGeneration = isJsonObject(data)
991
+ && Object.prototype.hasOwnProperty.call(data, 'inline_message')
992
+ ? this.captureInlineAckGeneration() : undefined;
993
+ this.runPushProcessSerialized(groupNs ? `group:${groupNs}` : '', () => this.processAndPublishGroupMessage(data, inlineGeneration), inlineGeneration).catch((exc) => {
851
994
  client._clientLog.warn(`group message decrypt failed: ${formatDeliveryError(exc)}`);
852
995
  if (isJsonObject(data)) {
853
996
  const src = data;
@@ -865,7 +1008,7 @@ export class MessageDeliveryEngine {
865
1008
  });
866
1009
  client._clientLog.debug(`_onRawGroupMessageCreated exit: elapsed=${Date.now() - tStart}ms (handler dispatched)`);
867
1010
  }
868
- async processAndPublishGroupMessage(data) {
1011
+ async processAndPublishGroupMessage(data, inlineGeneration) {
869
1012
  const client = this.runtime.client;
870
1013
  if (!isJsonObject(data)) {
871
1014
  await client._publishAppEvent('group.message_created', data, 'group-push');
@@ -878,6 +1021,21 @@ export class MessageDeliveryEngine {
878
1021
  if (groupId) {
879
1022
  client._groupSynced.add(groupId);
880
1023
  }
1024
+ // V2 会话中旧 group.message_created 也只是 Head 通知;必须先 Tail,
1025
+ // 不能让 payload 路径直接推进旧 ordered publish 状态。
1026
+ const inlinePresent = Object.prototype.hasOwnProperty.call(msg, 'inline_message');
1027
+ const legacyPushSeq = this.realtimeHeadSeq(seq, inlinePresent);
1028
+ if (client._v2Session && groupId && seq !== undefined && seq !== null) {
1029
+ if (legacyPushSeq === null) {
1030
+ if (inlinePresent)
1031
+ await this.recoverInvalidGroupRealtimeHead(groupId, `group:${groupId}`, seq);
1032
+ return;
1033
+ }
1034
+ await this.onRawGroupV2MessageCreated({ ...msg, seq: legacyPushSeq }, 'v1', inlineGeneration);
1035
+ if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
1036
+ return;
1037
+ return;
1038
+ }
881
1039
  if (payload === undefined || payload === null
882
1040
  || (typeof payload === 'object' && Object.keys(payload).length === 0)) {
883
1041
  void this.autoPullGroupMessages(msg).catch((exc) => {
@@ -911,7 +1069,7 @@ export class MessageDeliveryEngine {
911
1069
  : await this.publishOrderedMessage('group.message_created', ns, seq, msg);
912
1070
  const contigAfter = client._seqTracker.getContiguousSeq(ns);
913
1071
  const needPull = Number(seq) > contigAfter && !published;
914
- if (needPull) {
1072
+ if (needPull && client._sessionOptions?.background_sync !== false) {
915
1073
  client._clientLog.debug(`group message seq gap detected: group=${groupId}, seq=${seq}, contiguous=${contigAfter}`);
916
1074
  this.fillGroupGap(groupId).catch((exc) => client._clientLog.warn(`background gap fill trigger failed: ${formatDeliveryError(exc)}`));
917
1075
  }
@@ -944,22 +1102,32 @@ export class MessageDeliveryEngine {
944
1102
  await client._publishAppEvent('group.message_created', notification);
945
1103
  return;
946
1104
  }
947
- groupId = normalizeGroupId(groupId) || groupId;
1105
+ if (client._sessionOptions?.background_sync === false) {
1106
+ client._clientLog.debug(`auto pull group messages skipped: group=${groupId} background_sync=false`);
1107
+ return;
1108
+ }
1109
+ groupId = String(groupId ?? '').trim();
948
1110
  const ns = `group:${groupId}`;
949
1111
  const afterSeq = client._seqTracker.getContiguousSeq(ns);
950
1112
  client._clientLog.debug(`auto pull group messages start: group=${groupId}, after_seq=${afterSeq}, seq=${String(notification.seq ?? '')}`);
951
- const started = await client._tryRunBackgroundPull(ns, async () => {
1113
+ const operation = async () => {
952
1114
  const pullAfterSeq = client._seqTracker.getContiguousSeq(ns);
953
1115
  const messages = await client._pullGroupV2(groupId, pullAfterSeq, 50, { gateLocked: true });
954
1116
  this.prunePushedSeqs(ns);
955
1117
  return messages.length;
956
- }, true);
957
- if (!started) {
958
- client._clientLog.debug(`auto pull group messages skipped: pull in-flight group=${groupId}`);
1118
+ };
1119
+ if (client._rpcPipeline) {
1120
+ const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 });
1121
+ await client._rpcPipeline.runPullSerialized(key, operation, true);
1122
+ }
1123
+ else {
1124
+ await client._withBackgroundRpc(operation);
959
1125
  }
960
1126
  }
961
1127
  async fillP2pGap() {
962
1128
  const client = this.runtime.client;
1129
+ if (client._sessionOptions?.background_sync === false)
1130
+ return;
963
1131
  if (!client._aid)
964
1132
  return;
965
1133
  const ns = `p2p:${client._aid}`;
@@ -967,16 +1135,14 @@ export class MessageDeliveryEngine {
967
1135
  const dedupKey = `p2p:${afterSeq}`;
968
1136
  if (client._gapFillDone.has(dedupKey))
969
1137
  return;
970
- const token = client._tryAcquirePullGate(ns);
971
- if (token === null) {
972
- client._clientLog.debug(`P2P message gap fill skipped: pull in-flight ns=${ns}`);
973
- return;
974
- }
975
1138
  client._gapFillDone.set(dedupKey, Date.now());
976
1139
  client._clientLog.debug(`P2P message gap fill start: after_seq=${afterSeq}`);
977
1140
  let filled = 0;
978
1141
  try {
979
- const messages = await client._withBackgroundRpc(() => client._pullV2(afterSeq, 50, { skipAutoAck: true, gateLocked: true }));
1142
+ const operation = () => client._pullV2(afterSeq, 50, { skipAutoAck: true, gateLocked: true });
1143
+ const messages = client._rpcPipeline
1144
+ ? await client._rpcPipeline.runPullSerialized(client._rpcPipeline.pullGateKeyForCall('message.pull', { after_seq: afterSeq, limit: 50 }), operation, true)
1145
+ : await client._withBackgroundRpc(operation);
980
1146
  filled = messages.length;
981
1147
  this.prunePushedSeqs(ns);
982
1148
  if (client._seqTracker.getContiguousSeq(ns) !== afterSeq) {
@@ -994,12 +1160,13 @@ export class MessageDeliveryEngine {
994
1160
  }
995
1161
  finally {
996
1162
  client._gapFillDone.delete(dedupKey);
997
- client._releasePullGate(ns, token);
998
1163
  }
999
1164
  }
1000
1165
  async fillGroupGap(groupId) {
1001
1166
  const client = this.runtime.client;
1002
- groupId = normalizeGroupId(groupId) || String(groupId ?? '').trim();
1167
+ if (client._sessionOptions?.background_sync === false)
1168
+ return;
1169
+ groupId = String(groupId ?? '').trim();
1003
1170
  if (!groupId)
1004
1171
  return;
1005
1172
  const ns = `group:${groupId}`;
@@ -1007,16 +1174,14 @@ export class MessageDeliveryEngine {
1007
1174
  const dedupKey = `group_msg:${groupId}:${afterSeq}`;
1008
1175
  if (client._gapFillDone.has(dedupKey))
1009
1176
  return;
1010
- const token = client._tryAcquirePullGate(ns);
1011
- if (token === null) {
1012
- client._clientLog.debug(`group message gap fill skipped: pull in-flight group=${groupId}`);
1013
- return;
1014
- }
1015
1177
  client._gapFillDone.set(dedupKey, Date.now());
1016
1178
  client._clientLog.debug(`group message gap fill start: group=${groupId}, after_seq=${afterSeq}`);
1017
1179
  let filled = 0;
1018
1180
  try {
1019
- const messages = await client._withBackgroundRpc(() => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true }));
1181
+ const operation = () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true });
1182
+ const messages = client._rpcPipeline
1183
+ ? await client._rpcPipeline.runPullSerialized(client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 }), operation, true)
1184
+ : await client._withBackgroundRpc(operation);
1020
1185
  filled = messages.length;
1021
1186
  this.prunePushedSeqs(ns);
1022
1187
  if (client._seqTracker.getContiguousSeq(ns) !== afterSeq) {
@@ -1030,7 +1195,6 @@ export class MessageDeliveryEngine {
1030
1195
  }
1031
1196
  finally {
1032
1197
  client._gapFillDone.delete(dedupKey);
1033
- client._releasePullGate(ns, token);
1034
1198
  if (filled > 0 && client._seqTracker.getContiguousSeq(ns) > afterSeq) {
1035
1199
  void this.fillGroupGap(groupId);
1036
1200
  }
@@ -1038,7 +1202,9 @@ export class MessageDeliveryEngine {
1038
1202
  }
1039
1203
  async fillGroupEventGap(groupId) {
1040
1204
  const client = this.runtime.client;
1041
- groupId = normalizeGroupId(groupId) || String(groupId ?? '').trim();
1205
+ if (client._sessionOptions?.background_sync === false)
1206
+ return;
1207
+ groupId = String(groupId ?? '').trim();
1042
1208
  if (!groupId)
1043
1209
  return;
1044
1210
  const ns = `group_event:${groupId}`;
@@ -1082,12 +1248,16 @@ export class MessageDeliveryEngine {
1082
1248
  ? result.cursor
1083
1249
  : null;
1084
1250
  const cursorCurrentSeq = Number(cursor?.current_seq ?? 0);
1085
- const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_event_seq', 'retention_floor_event_seq'), Number.isFinite(cursorCurrentSeq) ? cursorCurrentSeq : 0);
1086
- if (retentionFloor > 0) {
1251
+ const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_event_seq', 'retention_floor_event_seq'));
1252
+ const serverAckFloor = Number.isFinite(cursorCurrentSeq) ? Math.max(0, cursorCurrentSeq) : 0;
1253
+ const earliestAvailable = Number(result.earliest_available_event_seq ?? 0);
1254
+ const visibilityFloor = Math.max(0, Number.isFinite(Number(cursor?.join_seq)) ? Number(cursor?.join_seq) : 0, Number.isFinite(earliestAvailable) && earliestAvailable > 0 ? earliestAvailable - 1 : 0);
1255
+ const effectiveFloor = Math.max(retentionFloor, serverAckFloor, visibilityFloor);
1256
+ if (effectiveFloor > 0) {
1087
1257
  const contigBeforeFloor = client._seqTracker.getContiguousSeq(ns);
1088
- if (contigBeforeFloor < retentionFloor) {
1089
- client._clientLog.info(`group.pull_events cursor/floor advance: ns=${ns} contiguous=${contigBeforeFloor} -> cursor.current_seq=${cursorCurrentSeq} floor=${retentionFloor}`);
1090
- client._seqTracker.forceContiguousSeq(ns, retentionFloor);
1258
+ if (contigBeforeFloor < effectiveFloor) {
1259
+ client._clientLog.info(`group.pull_events floor advance: ns=${ns} contiguous=${contigBeforeFloor} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} server_ack=${serverAckFloor} visibility_floor=${visibilityFloor}`);
1260
+ client._seqTracker.forceContiguousSeq(ns, effectiveFloor);
1091
1261
  }
1092
1262
  }
1093
1263
  const eventSeqs = [];
@@ -1155,7 +1325,7 @@ export class MessageDeliveryEngine {
1155
1325
  }
1156
1326
  async handleGroupChangedEventSeq(data, groupId) {
1157
1327
  const client = this.runtime.client;
1158
- groupId = normalizeGroupId(groupId) || String(groupId ?? '').trim();
1328
+ groupId = String(groupId ?? '').trim();
1159
1329
  let needPull = false;
1160
1330
  const rawEventSeq = data.event_seq;
1161
1331
  const eventSeq = Number(rawEventSeq);
@@ -1164,6 +1334,17 @@ export class MessageDeliveryEngine {
1164
1334
  return;
1165
1335
  }
1166
1336
  const ns = `group_event:${groupId}`;
1337
+ if (String(data.kind ?? '').trim() === 'group.online_unread_hint') {
1338
+ if (client._sessionOptions?.background_sync === false) {
1339
+ client._clientLog.debug(`group.changed online unread hint skipped: group=${groupId} background_sync=false`);
1340
+ return;
1341
+ }
1342
+ client._clientLog.debug(`group.changed online unread hint triggering gap fill: group=${groupId}, event_seq=${eventSeq}`);
1343
+ this.fillGroupEventGap(groupId).catch((exc) => {
1344
+ client._clientLog.warn(`background gap fill trigger failed: ${formatDeliveryError(exc)}`);
1345
+ });
1346
+ return;
1347
+ }
1167
1348
  if (this.isSelfJoinGroupChanged(data)) {
1168
1349
  const contig = client._seqTracker.getContiguousSeq(ns);
1169
1350
  const maxSeen = client._seqTracker.getMaxSeenSeq(ns);
@@ -1185,17 +1366,16 @@ export class MessageDeliveryEngine {
1185
1366
  if (ackContig > 0 && ackContig !== contigBefore) {
1186
1367
  if (data.action !== 'dissolved')
1187
1368
  this.persistSeq(ns);
1188
- const ackSeq = this.clampAckSeq('group.ack_events', 'event_seq', ns, ackContig);
1189
1369
  client._transport.call('group.ack_events', {
1190
1370
  group_id: groupId,
1191
- event_seq: ackSeq,
1371
+ event_seq: ackContig,
1192
1372
  device_id: client._deviceId,
1193
1373
  slot_id: client._slotId,
1194
1374
  }, undefined, undefined, true).catch((e) => {
1195
1375
  client._clientLog.debug(`group event push auto-ack failed: group=${groupId} ${formatDeliveryError(e)}`);
1196
1376
  });
1197
1377
  }
1198
- if (needPull && groupId && !data._from_gap_fill) {
1378
+ if (needPull && groupId && !data._from_gap_fill && client._sessionOptions?.background_sync !== false) {
1199
1379
  this.fillGroupEventGap(groupId).catch((exc) => {
1200
1380
  client._clientLog.warn(`background gap fill trigger failed: ${formatDeliveryError(exc)}`);
1201
1381
  });
@@ -1203,7 +1383,7 @@ export class MessageDeliveryEngine {
1203
1383
  }
1204
1384
  fireGroupEventAck(groupId, eventSeq, reason) {
1205
1385
  const client = this.runtime.client;
1206
- const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
1386
+ const gid = String(groupId ?? '').trim();
1207
1387
  if (!gid)
1208
1388
  return;
1209
1389
  const ns = `group_event:${gid}`;
@@ -1226,21 +1406,25 @@ export class MessageDeliveryEngine {
1226
1406
  client._clientLog.debug(`group event ${reason} ack failed: group=${gid} ${formatDeliveryError(e)}`);
1227
1407
  }
1228
1408
  }
1229
- fireGroupV2Ack(groupId, upToSeq, reason) {
1409
+ fireGroupV2Ack(groupId, upToSeq, reason, groupAid = '') {
1230
1410
  const client = this.runtime.client;
1231
- const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
1411
+ const gid = String(groupId ?? '').trim();
1232
1412
  if (!gid)
1233
1413
  return;
1234
1414
  const ns = `group:${gid}`;
1235
1415
  const ackSeq = this.clampAckSeq('group.v2.ack', 'up_to_seq', ns, Number(upToSeq) || 0);
1236
1416
  if (ackSeq <= 0)
1237
1417
  return;
1418
+ const generation = this.inlineAckGeneration;
1238
1419
  try {
1239
1420
  Promise.resolve(client.call('group.v2.ack', {
1240
1421
  group_id: gid,
1422
+ ...(groupAid ? { group_aid: groupAid } : {}),
1241
1423
  up_to_seq: ackSeq,
1242
1424
  _rpc_background: true,
1243
- })).catch((e) => {
1425
+ })).then((result) => {
1426
+ this.onGroupV2AckResult(gid, result, ackSeq, generation);
1427
+ }).catch((e) => {
1244
1428
  client._clientLog.debug(`group.v2 ${reason} ack failed: group=${gid} ${formatDeliveryError(e)}`);
1245
1429
  });
1246
1430
  }
@@ -1261,84 +1445,948 @@ export class MessageDeliveryEngine {
1261
1445
  const actorAid = String(data.actor_aid ?? '').trim();
1262
1446
  return !joinedAid && ['joined', 'invite_code_used'].includes(action) && actorAid === selfAid;
1263
1447
  }
1264
- async onV2PushNotification(data) {
1448
+ inlinePostAckRow(row) {
1449
+ const result = {};
1450
+ for (const key of ['envelope_json', 'spk_id', 'group_id', 'message_id']) {
1451
+ const value = row[key];
1452
+ if (typeof value === 'string' && value)
1453
+ result[key] = value;
1454
+ }
1455
+ const seq = this.inlineSafePositiveSeq(row.seq);
1456
+ if (seq !== null)
1457
+ result.seq = seq;
1458
+ return result;
1459
+ }
1460
+ reservePendingGroupInlineAck(ns, seq, row, groupAid) {
1461
+ if (!ns || !Number.isSafeInteger(seq) || seq <= 0 || this.pendingGroupInlineAcks?.has(ns)
1462
+ || this.pendingInlineAckNamespaceCount() >= MAX_INLINE_PENDING_ACK_NAMESPACES)
1463
+ return false;
1464
+ const normalizedGroupAid = typeof groupAid === 'string' ? groupAid.trim() : '';
1465
+ const aliasNs = normalizedGroupAid ? `group:${normalizedGroupAid}` : '';
1466
+ const namespaces = aliasNs && aliasNs !== ns ? [ns, aliasNs] : [ns];
1467
+ const pendingMap = this.pendingGroupInlineAcks ?? new Map();
1468
+ const namespaceMap = this.pendingGroupInlineAckNamespaces ?? new Map();
1469
+ this.pendingGroupInlineAcks = pendingMap;
1470
+ this.pendingGroupInlineAckNamespaces = namespaceMap;
1471
+ pendingMap.set(ns, {
1472
+ seq,
1473
+ row: this.inlinePostAckRow(row),
1474
+ groupAid: normalizedGroupAid,
1475
+ namespaces,
1476
+ acking: false,
1477
+ committed: false,
1478
+ observedAckSeq: 0,
1479
+ });
1480
+ for (const namespace of namespaces) {
1481
+ let candidates = namespaceMap.get(namespace);
1482
+ if (!candidates) {
1483
+ candidates = new Set();
1484
+ namespaceMap.set(namespace, candidates);
1485
+ }
1486
+ candidates.add(ns);
1487
+ }
1488
+ return true;
1489
+ }
1490
+ removePendingGroupInlineAck(ns, pending) {
1491
+ const pendingMap = this.pendingGroupInlineAcks;
1492
+ if (pendingMap?.get(ns) !== pending)
1493
+ return false;
1494
+ pendingMap.delete(ns);
1495
+ for (const namespace of pending.namespaces) {
1496
+ const aliases = this.pendingGroupInlineAckNamespaces?.get(namespace);
1497
+ if (!aliases)
1498
+ continue;
1499
+ aliases.delete(ns);
1500
+ if (aliases.size === 0)
1501
+ this.pendingGroupInlineAckNamespaces?.delete(namespace);
1502
+ }
1503
+ return true;
1504
+ }
1505
+ dropPendingGroupInlineAck(ns, seq) {
1506
+ const pending = this.pendingGroupInlineAcks?.get(ns);
1507
+ if (pending?.seq === seq)
1508
+ this.removePendingGroupInlineAck(ns, pending);
1509
+ }
1510
+ resolvePendingGroupInlineAckNamespace(ns) {
1511
+ const candidates = this.pendingGroupInlineAckNamespaces?.get(ns);
1512
+ if (!candidates || candidates.size !== 1)
1513
+ return ns;
1514
+ const canonicalNs = candidates.values().next().value;
1515
+ return typeof canonicalNs === 'string' && Boolean(this.pendingGroupInlineAcks?.has(canonicalNs))
1516
+ ? canonicalNs
1517
+ : ns;
1518
+ }
1519
+ completePendingGroupInlineAck(ns, upToSeq) {
1520
+ if (!ns || !Number.isSafeInteger(upToSeq) || upToSeq <= 0)
1521
+ return;
1522
+ const candidates = this.pendingGroupInlineAckNamespaces?.get(ns);
1523
+ if (!candidates || candidates.size !== 1)
1524
+ return;
1525
+ const canonicalNs = candidates.values().next().value;
1526
+ if (typeof canonicalNs !== 'string')
1527
+ return;
1528
+ const pending = this.pendingGroupInlineAcks?.get(canonicalNs);
1529
+ if (!pending || upToSeq < pending.seq)
1530
+ return;
1531
+ if (!pending.committed) {
1532
+ pending.observedAckSeq = Math.max(pending.observedAckSeq, upToSeq);
1533
+ return;
1534
+ }
1535
+ this.removePendingGroupInlineAck(canonicalNs, pending);
1536
+ this.runPostInlineCommit(pending.row);
1537
+ }
1538
+ markPendingGroupInlineAckCommitted(ns, seq) {
1539
+ const pending = this.pendingGroupInlineAcks?.get(ns);
1540
+ if (!pending || pending.seq !== seq)
1541
+ return false;
1542
+ pending.committed = true;
1543
+ if (pending.observedAckSeq < seq)
1544
+ return true;
1545
+ this.removePendingGroupInlineAck(ns, pending);
1546
+ this.runPostInlineCommit(pending.row);
1547
+ return true;
1548
+ }
1549
+ retryPendingGroupInlineAck(ns, seq, reason, ackThrough = seq) {
1550
+ const pending = this.pendingGroupInlineAcks?.get(ns);
1551
+ if (!pending || pending.seq !== seq)
1552
+ return false;
1553
+ if (pending.acking || !pending.committed)
1554
+ return true;
1555
+ const groupId = ns.startsWith('group:') ? ns.slice('group:'.length) : '';
1556
+ if (!groupId)
1557
+ return true;
1558
+ pending.acking = true;
1559
+ const requestedSeq = Math.max(seq, ackThrough);
1560
+ const generation = this.inlineAckGeneration;
1561
+ try {
1562
+ Promise.resolve(this.runtime.client.call('group.v2.ack', {
1563
+ group_id: groupId,
1564
+ ...(pending.groupAid ? { group_aid: pending.groupAid } : {}),
1565
+ up_to_seq: requestedSeq,
1566
+ _rpc_background: true,
1567
+ })).then((result) => {
1568
+ this.onGroupV2AckResult(groupId, result, requestedSeq, generation);
1569
+ }).catch((error) => {
1570
+ const current = this.pendingGroupInlineAcks?.get(ns);
1571
+ if (current === pending)
1572
+ current.acking = false;
1573
+ this.runtime.client._clientLog.debug(`group.v2 ${reason} ack failed: group=${groupId} ${formatDeliveryError(error)}`);
1574
+ }).finally(() => {
1575
+ const current = this.pendingGroupInlineAcks?.get(ns);
1576
+ if (current === pending)
1577
+ current.acking = false;
1578
+ });
1579
+ }
1580
+ catch (error) {
1581
+ pending.acking = false;
1582
+ this.runtime.client._clientLog.debug(`group.v2 ${reason} ack failed: group=${groupId} ${formatDeliveryError(error)}`);
1583
+ }
1584
+ return true;
1585
+ }
1586
+ runPostInlineCommit(row) {
1265
1587
  const client = this.runtime.client;
1266
- if (!client._v2Session)
1588
+ if (typeof client._postInlineV2CommitAndAck === 'function') {
1589
+ client._postInlineV2CommitAndAck(row);
1590
+ }
1591
+ }
1592
+ onGroupV2AckSucceeded(groupId, upToSeq, generation = this.inlineAckGeneration) {
1593
+ if (!this.inlineGenerationIsCurrent(generation))
1267
1594
  return;
1268
- const pushSeq = isJsonObject(data) ? Number(data.seq ?? 0) || 0 : 0;
1269
- const pushFrom = isJsonObject(data) ? String(data.from_aid ?? '') : '';
1270
- const pushMsgId = isJsonObject(data) ? String(data.message_id ?? '') : '';
1271
- const envelopeJson = isJsonObject(data) ? data.envelope_json : undefined;
1272
- const hasPayload = !!envelopeJson;
1273
- const ns = client._aid ? `p2p:${client._aid}` : '';
1274
- let contigBefore = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
1275
- client._clientLog.debug(`_onV2PushNotification: push_seq=${pushSeq || 'null'} push_from=${pushFrom} push_msg_id=${pushMsgId} has_payload=${hasPayload} contiguous_seq=${contigBefore}`);
1276
- if (pushSeq > 0 && ns) {
1277
- client._seqTracker.updateMaxSeen(ns, pushSeq);
1278
- if (contigBefore === pushSeq) {
1279
- client._clientLog.debug(`_onV2PushNotification: push seq=${pushSeq} already covered by contiguous_seq=${contigBefore}, ignore duplicate push`);
1280
- return;
1595
+ const gid = String(groupId ?? '').trim();
1596
+ if (!gid || !Number.isSafeInteger(upToSeq) || upToSeq <= 0)
1597
+ return;
1598
+ this.completePendingGroupInlineAck(`group:${gid}`, upToSeq);
1599
+ }
1600
+ onGroupV2AckResult(groupId, result, requestedSeq, generation = this.inlineAckGeneration) {
1601
+ if (!this.pendingGroupInlineAcks?.size || !this.inlineGenerationIsCurrent(generation))
1602
+ return;
1603
+ this.onGroupV2AckSucceeded(groupId, this.resolveActualAckSeq(result, requestedSeq), generation);
1604
+ }
1605
+ onGroupV2PullAckResult(groupId, result, requestedSeq, generation) {
1606
+ if (!this.pendingGroupInlineAcks?.size || !this.inlineGenerationIsCurrent(generation)
1607
+ || !Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
1608
+ return;
1609
+ let actualAckSeq = requestedSeq;
1610
+ const response = isJsonObject(result) ? result : null;
1611
+ const cursor = response && isJsonObject(response.cursor)
1612
+ ? response.cursor
1613
+ : null;
1614
+ if (cursor && Object.prototype.hasOwnProperty.call(cursor, 'current_seq')) {
1615
+ const raw = cursor.current_seq;
1616
+ actualAckSeq = typeof raw === 'number' && Number.isSafeInteger(raw) && raw >= 0 ? raw : 0;
1617
+ }
1618
+ this.onGroupV2AckSucceeded(groupId, actualAckSeq, generation);
1619
+ }
1620
+ resolveActualAckSeq(result, requestedSeq) {
1621
+ if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
1622
+ return 0;
1623
+ const response = isJsonObject(result) ? result : null;
1624
+ if (!response)
1625
+ return requestedSeq;
1626
+ let raw;
1627
+ if (Object.prototype.hasOwnProperty.call(response, 'effective_ack_seq'))
1628
+ raw = response.effective_ack_seq;
1629
+ else if (Object.prototype.hasOwnProperty.call(response, 'ack_seq'))
1630
+ raw = response.ack_seq;
1631
+ else if (Object.prototype.hasOwnProperty.call(response, 'cursor')) {
1632
+ const cursor = response.cursor;
1633
+ const cursorObject = isJsonObject(cursor) ? cursor : null;
1634
+ if (cursorObject) {
1635
+ if (!Object.prototype.hasOwnProperty.call(cursorObject, 'current_seq'))
1636
+ return 0;
1637
+ raw = cursorObject.current_seq;
1638
+ }
1639
+ else {
1640
+ raw = cursor;
1281
1641
  }
1282
- contigBefore = client._repairPushContiguousBound(ns, pushSeq, hasPayload, '_raw.peer.v2.message_received');
1283
1642
  }
1284
- if (hasPayload && pushSeq > 0 && ns) {
1285
- try {
1286
- const decrypted = await client._decryptV2PushMessage(data);
1287
- if (decrypted) {
1288
- const published = await client._publishOrderedMessage('message.received', ns, pushSeq, decrypted);
1289
- const newContig = client._seqTracker.getContiguousSeq(ns);
1290
- const needPull = pushSeq > newContig && !published;
1291
- if (newContig !== contigBefore) {
1292
- this.persistSeq(ns);
1293
- }
1294
- if (newContig > 0 && newContig !== contigBefore) {
1295
- const ackSeq = this.clampAckSeq('message.v2.ack', 'up_to_seq', ns, newContig);
1296
- try {
1297
- await client.call('message.v2.ack', { up_to_seq: ackSeq, _rpc_background: true });
1298
- }
1299
- catch (e) {
1300
- client._clientLog.debug(`V2 P2P push-ack failed: ${formatDeliveryError(e)}`);
1301
- }
1302
- }
1303
- client._clientLog.debug(`_onV2PushNotification: push payload 解密成功, contiguous_seq=${contigBefore}->${newContig} push_seq=${pushSeq}`);
1304
- if (!needPull && (published || newContig >= pushSeq || pushSeq <= contigBefore)) {
1305
- return;
1306
- }
1307
- client._clientLog.debug(`_onV2PushNotification: payload push seq=${pushSeq} 因空洞挂起,继续 pull 补齐 after_seq=${newContig}`);
1643
+ else
1644
+ return requestedSeq;
1645
+ return typeof raw === 'number' && Number.isSafeInteger(raw) && raw >= 0 ? raw : 0;
1646
+ }
1647
+ async runP2PRealtimeSync(ns, pushSeq, pageLimit = 50, forceTail = false) {
1648
+ const client = this.runtime.client;
1649
+ const before = this.syncState(ns);
1650
+ const order = forceTail ? ['tail'] : typeof client._seqTracker.planRealtimeSync === 'function'
1651
+ ? client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit)
1652
+ : (pushSeq > before.ack ? ['tail'] : []);
1653
+ let result = { messages: [], raw_count: 0 };
1654
+ let tailHead = null;
1655
+ const run = async (request, background) => {
1656
+ const invoke = async () => {
1657
+ const live = this.syncState(ns);
1658
+ if (request.window_mode === 'tail' && !forceTail && typeof client._seqTracker.getSyncState === 'function' && pushSeq <= live.head)
1659
+ return { messages: [], raw_count: 0 };
1660
+ if (request.window_mode !== 'tail' && live.ack >= live.tail - 1)
1661
+ return { messages: [], raw_count: 0 };
1662
+ if (request.window_mode === 'tail') {
1663
+ const tailResult = await client._v2E2EE.pullV2TailInternal({
1664
+ window_mode: 'tail',
1665
+ after_seq: live.ack,
1666
+ limit: pageLimit,
1667
+ });
1668
+ tailHead = this.syncState(ns).head;
1669
+ return tailResult;
1670
+ }
1671
+ const after = live.ack;
1672
+ const opts = { gateLocked: true };
1673
+ if (request.max_pages !== undefined)
1674
+ opts.maxPages = request.max_pages;
1675
+ const messages = await client._pullV2(after, pageLimit, opts);
1676
+ return { messages, raw_count: messages.length };
1677
+ };
1678
+ if (!client._rpcPipeline)
1679
+ return await invoke();
1680
+ const key = client._rpcPipeline.pullGateKeyForCall('message.pull', request);
1681
+ return await client._rpcPipeline.runPullSerialized(key, invoke, background);
1682
+ };
1683
+ if (order.length === 1 && order[0] === 'tail') {
1684
+ result = await run({ window_mode: 'tail', after_seq: this.syncState(ns).ack, limit: pageLimit }, false);
1685
+ }
1686
+ const state = this.syncState(ns);
1687
+ const needsForward = state.ack < state.tail - 1;
1688
+ const backgroundEnabled = client._sessionOptions?.background_sync !== false;
1689
+ if (backgroundEnabled && (order.length === 1 && order[0] === 'forward' || (order.length === 1 && order[0] === 'tail' && needsForward))) {
1690
+ result = await run({ after_seq: state.ack, limit: pageLimit, ...(order[0] === 'tail' ? { max_pages: 1 } : {}) }, true);
1691
+ }
1692
+ if (tailHead !== null) {
1693
+ this.markRealtimeTailCompleted(result, tailHead);
1694
+ }
1695
+ return result;
1696
+ }
1697
+ async runGroupRealtimeSync(groupId, ns, pushSeq, pageLimit = 50, forceTail = false) {
1698
+ const client = this.runtime.client;
1699
+ const before = this.syncState(ns);
1700
+ const order = forceTail ? ['tail'] : typeof client._seqTracker.planRealtimeSync === 'function'
1701
+ ? client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit)
1702
+ : (pushSeq > before.ack ? ['tail'] : []);
1703
+ let result = { messages: [], raw_count: 0 };
1704
+ let tailHead = null;
1705
+ const run = async (request, background) => {
1706
+ const invoke = async () => {
1707
+ const live = this.syncState(ns);
1708
+ if (request.window_mode === 'tail' && !forceTail && typeof client._seqTracker.getSyncState === 'function' && pushSeq <= live.head)
1709
+ return { messages: [], raw_count: 0 };
1710
+ if (request.window_mode !== 'tail' && live.ack >= live.tail - 1)
1711
+ return { messages: [], raw_count: 0 };
1712
+ if (request.window_mode === 'tail') {
1713
+ const tailResult = await client._v2E2EE.pullGroupV2TailInternal({
1714
+ group_id: groupId,
1715
+ window_mode: 'tail',
1716
+ after_seq: live.ack,
1717
+ limit: pageLimit,
1718
+ });
1719
+ tailHead = this.syncState(ns).head;
1720
+ return tailResult;
1308
1721
  }
1722
+ const opts = { gateLocked: true };
1723
+ if (request.max_pages !== undefined)
1724
+ opts.maxPages = request.max_pages;
1725
+ const messages = await client._pullGroupV2(groupId, live.ack, pageLimit, opts);
1726
+ return { messages, raw_count: messages.length };
1727
+ };
1728
+ if (!client._rpcPipeline)
1729
+ return await invoke();
1730
+ const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, ...request });
1731
+ return await client._rpcPipeline.runPullSerialized(key, invoke, background);
1732
+ };
1733
+ if (order.length === 1 && order[0] === 'tail') {
1734
+ result = await run({ window_mode: 'tail', after_seq: this.syncState(ns).ack, limit: pageLimit }, false);
1735
+ }
1736
+ const state = this.syncState(ns);
1737
+ const needsForward = state.ack < state.tail - 1;
1738
+ const backgroundEnabled = client._sessionOptions?.background_sync !== false;
1739
+ if (backgroundEnabled && (order.length === 1 && order[0] === 'forward' || (order.length === 1 && order[0] === 'tail' && needsForward))) {
1740
+ result = await run({ after_seq: state.ack, limit: pageLimit, ...(order[0] === 'tail' ? { max_pages: 1 } : {}) }, true);
1741
+ }
1742
+ if (tailHead !== null) {
1743
+ this.markRealtimeTailCompleted(result, tailHead);
1744
+ }
1745
+ return result;
1746
+ }
1747
+ async runP2PForwardRecovery(ns, pageLimit) {
1748
+ const client = this.runtime.client;
1749
+ const request = { after_seq: this.syncState(ns).ack, limit: pageLimit, max_pages: 1 };
1750
+ const invoke = async () => {
1751
+ const after = this.syncState(ns).ack;
1752
+ const messages = await client._pullV2(after, pageLimit, { gateLocked: true, maxPages: 1 });
1753
+ return { messages, raw_count: messages.length };
1754
+ };
1755
+ if (!client._rpcPipeline)
1756
+ return await invoke();
1757
+ const key = client._rpcPipeline.pullGateKeyForCall('message.pull', request);
1758
+ return await client._rpcPipeline.runPullSerialized(key, invoke, true);
1759
+ }
1760
+ async runGroupForwardRecovery(groupId, ns, pageLimit) {
1761
+ const client = this.runtime.client;
1762
+ const request = { group_id: groupId, after_seq: this.syncState(ns).ack, limit: pageLimit, max_pages: 1 };
1763
+ const invoke = async () => {
1764
+ const after = this.syncState(ns).ack;
1765
+ const messages = await client._pullGroupV2(groupId, after, pageLimit, { gateLocked: true, maxPages: 1 });
1766
+ return { messages, raw_count: messages.length };
1767
+ };
1768
+ if (!client._rpcPipeline)
1769
+ return await invoke();
1770
+ const key = client._rpcPipeline.pullGateKeyForCall('group.pull', request);
1771
+ return await client._rpcPipeline.runPullSerialized(key, invoke, true);
1772
+ }
1773
+ syncState(ns) {
1774
+ const tracker = this.runtime.client._seqTracker;
1775
+ if (typeof tracker.getSyncState === 'function')
1776
+ return tracker.getSyncState(ns);
1777
+ const ack = Number(tracker.getContiguousSeq(ns) || 0);
1778
+ const maxSeen = Number(tracker.getMaxSeenSeq?.(ns) || 0);
1779
+ return { ack, tail: ack, head: Math.max(ack, maxSeen) };
1780
+ }
1781
+ inlineEnvelope(row) {
1782
+ const raw = row.envelope_json;
1783
+ if (typeof raw !== 'string' || !raw.trim())
1784
+ return null;
1785
+ try {
1786
+ const parsed = JSON.parse(raw);
1787
+ return isJsonObject(parsed) ? parsed : null;
1788
+ }
1789
+ catch {
1790
+ return null;
1791
+ }
1792
+ }
1793
+ inlineRowKind(row) {
1794
+ if (row.version === 'v1') {
1795
+ if (Object.prototype.hasOwnProperty.call(row, 'envelope_json'))
1796
+ return 'invalid';
1797
+ return 'v1';
1798
+ }
1799
+ if (row.version === 'v2') {
1800
+ if (typeof row.envelope_json !== 'string' || row.envelope_json.length === 0)
1801
+ return 'invalid';
1802
+ if (Object.prototype.hasOwnProperty.call(row, 'legacy_v1')
1803
+ || Object.prototype.hasOwnProperty.call(row, 'payload'))
1804
+ return 'invalid';
1805
+ return 'v2';
1806
+ }
1807
+ return 'invalid';
1808
+ }
1809
+ inlineTargetBindingMatches(head, row, envelope) {
1810
+ const client = this.runtime.client;
1811
+ if (!this.messageTargetsCurrentInstance(head))
1812
+ return false;
1813
+ const targetValue = (source, targetKey, legacyKey) => {
1814
+ if (Object.prototype.hasOwnProperty.call(source, targetKey)) {
1815
+ return typeof source[targetKey] === 'string' ? source[targetKey].trim() : '';
1309
1816
  }
1310
- catch (exc) {
1311
- client._clientLog.debug(`_onV2PushNotification: push payload 解密失败, fallback to pull: ${formatDeliveryError(exc)}`);
1817
+ if (Object.prototype.hasOwnProperty.call(source, legacyKey)) {
1818
+ return typeof source[legacyKey] === 'string' ? source[legacyKey].trim() : '';
1819
+ }
1820
+ return null;
1821
+ };
1822
+ const headDevice = targetValue(head, 'target_device_id', 'device_id');
1823
+ if (!headDevice || headDevice !== client._deviceId)
1824
+ return false;
1825
+ const rowDevice = targetValue(row, 'target_device_id', 'device_id');
1826
+ if (rowDevice !== null && (!rowDevice || rowDevice !== headDevice))
1827
+ return false;
1828
+ const localSlot = slotIsolationKey(client._slotId);
1829
+ const headSlot = targetValue(head, 'target_slot_id', 'slot_id');
1830
+ if (headSlot !== null && slotIsolationKey(headSlot) !== localSlot)
1831
+ return false;
1832
+ const rowSlot = targetValue(row, 'target_slot_id', 'slot_id');
1833
+ if (rowSlot !== null && (!rowSlot || slotIsolationKey(rowSlot) !== localSlot))
1834
+ return false;
1835
+ if (headSlot !== null && rowSlot !== null && slotIsolationKey(rowSlot) !== slotIsolationKey(headSlot))
1836
+ return false;
1837
+ if (!envelope)
1838
+ return true;
1839
+ if (Object.prototype.hasOwnProperty.call(envelope, 'recipients'))
1840
+ return false;
1841
+ if (!isJsonObject(envelope.recipient))
1842
+ return false;
1843
+ const recipient = envelope.recipient;
1844
+ if (typeof recipient.aid !== 'string' || typeof recipient.device_id !== 'string'
1845
+ || typeof client._aid !== 'string' || typeof client._deviceId !== 'string')
1846
+ return false;
1847
+ const recipientAid = recipient.aid;
1848
+ const recipientDevice = recipient.device_id;
1849
+ if (!recipientAid || recipientAid !== client._aid || !client._deviceId)
1850
+ return false;
1851
+ const hasDeliveryDevice = Object.prototype.hasOwnProperty.call(envelope, 'delivery_device_id');
1852
+ const deliveryDevice = envelope.delivery_device_id;
1853
+ if (hasDeliveryDevice
1854
+ && (typeof deliveryDevice !== 'string' || !deliveryDevice || deliveryDevice !== client._deviceId))
1855
+ return false;
1856
+ if (recipientDevice)
1857
+ return recipientDevice === client._deviceId;
1858
+ return hasDeliveryDevice;
1859
+ }
1860
+ inlineRequiredBinding(left, leftKeys, right, rightKeys) {
1861
+ const leftValue = this.consistentBindingValue(left, leftKeys);
1862
+ const rightValue = this.consistentBindingValue(right, rightKeys);
1863
+ return leftValue !== null && rightValue !== null && leftValue === rightValue;
1864
+ }
1865
+ consistentBindingValue(source, keys) {
1866
+ let value = null;
1867
+ for (const key of keys) {
1868
+ if (!Object.prototype.hasOwnProperty.call(source, key))
1869
+ continue;
1870
+ if (typeof source[key] !== 'string')
1871
+ return null;
1872
+ const candidate = source[key].trim();
1873
+ if (!candidate || (value !== null && candidate !== value))
1874
+ return null;
1875
+ value = candidate;
1876
+ }
1877
+ return value;
1878
+ }
1879
+ inlineHeadMatches(head, row, pushSeq, groupId = '') {
1880
+ const rowSeq = this.inlineSafePositiveSeq(row.seq);
1881
+ if (rowSeq === null || rowSeq !== pushSeq)
1882
+ return false;
1883
+ if (!this.inlineRequiredBinding(head, ['message_id'], row, ['message_id']))
1884
+ return false;
1885
+ if (groupId) {
1886
+ const rowGroupId = typeof row.group_id === 'string' ? row.group_id.trim() : '';
1887
+ if (!rowGroupId || rowGroupId !== groupId)
1888
+ return false;
1889
+ if (Object.prototype.hasOwnProperty.call(head, 'group_aid')
1890
+ && (typeof head.group_aid !== 'string' || !head.group_aid.trim()))
1891
+ return false;
1892
+ }
1893
+ if (!this.inlineRequiredBinding(head, ['from_aid', 'sender_aid', 'from'], row, ['from_aid', 'sender_aid', 'from'])
1894
+ || !this.inlineRequiredBinding(head, ['sender_device_id'], row, ['sender_device_id']))
1895
+ return false;
1896
+ const kind = this.inlineRowKind(row);
1897
+ if (kind === 'invalid')
1898
+ return false;
1899
+ const envelope = kind === 'v2' ? this.inlineEnvelope(row) : null;
1900
+ if (kind === 'v2' && !envelope)
1901
+ return false;
1902
+ if (!this.inlineTargetBindingMatches(head, row, envelope))
1903
+ return false;
1904
+ const logicalTo = groupId ? null : this.consistentBindingValue(head, ['to_aid', 'to']);
1905
+ if (!groupId && !logicalTo)
1906
+ return false;
1907
+ if (kind === 'v1') {
1908
+ if (groupId) {
1909
+ if (Object.prototype.hasOwnProperty.call(row, 'legacy_v1'))
1910
+ return false;
1911
+ }
1912
+ else {
1913
+ if (Object.prototype.hasOwnProperty.call(row, 'payload'))
1914
+ return false;
1915
+ const legacy = isJsonObject(row.legacy_v1)
1916
+ ? row.legacy_v1
1917
+ : null;
1918
+ const legacyTo = legacy ? this.consistentBindingValue(legacy, ['to']) : null;
1919
+ if (!legacyTo || legacyTo !== logicalTo)
1920
+ return false;
1312
1921
  }
1313
1922
  }
1314
- if (pushSeq > 0 && ns) {
1315
- client._clientLog.debug(`_onV2PushNotification: 纯通知 push_seq=${pushSeq} > contiguous_seq=${contigBefore}, 触发 pull(after_seq=${contigBefore})`);
1923
+ if (envelope) {
1924
+ const envelopeType = typeof envelope.type === 'string' ? envelope.type.trim() : '';
1925
+ const envelopeVersion = typeof envelope.version === 'string' ? envelope.version.trim() : '';
1926
+ const hasEnvelopeGroupId = Object.prototype.hasOwnProperty.call(envelope, 'group_id');
1927
+ const envelopeGroupId = typeof envelope.group_id === 'string' ? envelope.group_id.trim() : '';
1928
+ if (groupId) {
1929
+ if (envelopeType !== 'e2ee.group_encrypted'
1930
+ || envelopeVersion !== 'v2'
1931
+ || (hasEnvelopeGroupId && envelopeGroupId !== groupId))
1932
+ return false;
1933
+ }
1934
+ else if (envelopeType !== 'e2ee.p2p_encrypted'
1935
+ || envelopeVersion !== 'v2'
1936
+ || (hasEnvelopeGroupId
1937
+ && (typeof envelope.group_id !== 'string' || envelopeGroupId !== '')))
1938
+ return false;
1939
+ if (!isJsonObject(envelope.aad))
1940
+ return false;
1941
+ const aad = envelope.aad;
1942
+ if (!this.inlineRequiredBinding(aad, ['message_id'], row, ['message_id'])
1943
+ || !this.inlineRequiredBinding(aad, ['from'], row, ['from_aid', 'sender_aid', 'from'])
1944
+ || !this.inlineRequiredBinding(aad, ['from_device'], row, ['sender_device_id']))
1945
+ return false;
1946
+ const aadGroupId = this.consistentBindingValue(aad, ['group_id']);
1947
+ if (groupId) {
1948
+ if (aadGroupId !== groupId)
1949
+ return false;
1950
+ }
1951
+ else {
1952
+ if ((Object.prototype.hasOwnProperty.call(aad, 'group_id') && aadGroupId !== '')
1953
+ || this.consistentBindingValue(aad, ['to']) !== logicalTo)
1954
+ return false;
1955
+ }
1316
1956
  }
1317
- if (!ns)
1957
+ return true;
1958
+ }
1959
+ inlineSafePositiveSeq(value) {
1960
+ return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : null;
1961
+ }
1962
+ realtimeHeadSeq(value, inlinePresent) {
1963
+ const strict = this.inlineSafePositiveSeq(value);
1964
+ if (strict !== null || inlinePresent || typeof value !== 'string' || !value.trim())
1965
+ return strict;
1966
+ return this.inlineSafePositiveSeq(Number(value));
1967
+ }
1968
+ async recoverInvalidP2PRealtimeHead(ns, rawSeq) {
1969
+ const client = this.runtime.client;
1970
+ client._clientLog.warn(`V2 P2P push has invalid seq; forcing authoritative Tail: seq=${String(rawSeq)}`);
1971
+ if (!this.beginRealtimeSync(ns))
1318
1972
  return;
1319
- await client._tryRunBackgroundPull(ns, async () => {
1320
- const operationBefore = client._seqTracker.getContiguousSeq(ns);
1321
- const dedupKey = `p2p_pull:${ns}`;
1322
- if (client._gapFillDone.has(dedupKey)) {
1323
- client._recordPendingP2pPull(ns, pushSeq);
1324
- return 0;
1973
+ try {
1974
+ await this.runP2PRealtimeSync(ns, this.syncState(ns).head, 50, true);
1975
+ }
1976
+ catch (exc) {
1977
+ client._clientLog.warn(`V2 P2P invalid-seq Tail failed: ${formatDeliveryError(exc)}`);
1978
+ }
1979
+ finally {
1980
+ this.endRealtimeSync(ns);
1981
+ }
1982
+ }
1983
+ async recoverInvalidGroupRealtimeHead(groupId, ns, rawSeq) {
1984
+ const client = this.runtime.client;
1985
+ client._clientLog.warn(`V2 group push has invalid seq; forcing authoritative Tail: group=${groupId} seq=${String(rawSeq)}`);
1986
+ if (!this.beginRealtimeSync(ns))
1987
+ return;
1988
+ try {
1989
+ await this.runGroupRealtimeSync(groupId, ns, this.syncState(ns).head, 50, true);
1990
+ }
1991
+ catch (exc) {
1992
+ client._clientLog.warn(`V2 group invalid-seq Tail failed: group=${groupId} ${formatDeliveryError(exc)}`);
1993
+ }
1994
+ finally {
1995
+ this.endRealtimeSync(ns);
1996
+ }
1997
+ }
1998
+ inlineStateIsCaughtUp(ns, pushSeq) {
1999
+ const client = this.runtime.client;
2000
+ const state = this.syncState(ns);
2001
+ if (state.ack !== state.tail || state.tail !== state.head || pushSeq !== state.ack + 1)
2002
+ return false;
2003
+ if (Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? state.head) > state.head)
2004
+ return false;
2005
+ const pending = client._pendingOrderedMsgs.get(ns);
2006
+ return !pending || pending.size === 0;
2007
+ }
2008
+ async decodeInlineP2P(row) {
2009
+ const client = this.runtime.client;
2010
+ if (this.inlineRowKind(row) === 'v2') {
2011
+ const decrypted = await client._decryptV2PushMessage(row);
2012
+ return isJsonObject(decrypted) ? decrypted : null;
2013
+ }
2014
+ const legacy = isJsonObject(row.legacy_v1) ? row.legacy_v1 : null;
2015
+ const payload = legacy?.payload;
2016
+ if (!legacy || payload === undefined || payload === null)
2017
+ return null;
2018
+ const payloadType = isJsonObject(payload)
2019
+ ? String(payload.type ?? '').trim()
2020
+ : '';
2021
+ if (payloadType === 'e2ee.encrypted' || payloadType === 'e2ee.group_encrypted')
2022
+ return null;
2023
+ const fromAid = String(row.from_aid ?? '').trim();
2024
+ const seq = this.inlineSafePositiveSeq(row.seq);
2025
+ if (seq === null)
2026
+ return null;
2027
+ const result = {
2028
+ message_id: String(row.message_id ?? ''),
2029
+ from: fromAid,
2030
+ to: String(legacy.to ?? client._aid ?? ''),
2031
+ seq,
2032
+ type: String(row.type ?? ''),
2033
+ timestamp: row.t_server,
2034
+ payload: payload,
2035
+ encrypted: false,
2036
+ direction: String(row.direction ?? '').trim() || (fromAid && fromAid === client._aid ? 'outbound_sync' : 'inbound'),
2037
+ };
2038
+ if (row.sender_device_id !== undefined)
2039
+ result.sender_device_id = row.sender_device_id;
2040
+ return result;
2041
+ }
2042
+ async decodeInlineGroup(row, groupId, groupAid) {
2043
+ const client = this.runtime.client;
2044
+ if (this.inlineRowKind(row) === 'v2') {
2045
+ const decrypted = await client._decryptV2PushMessage(row);
2046
+ if (!isJsonObject(decrypted))
2047
+ return null;
2048
+ const decoded = decrypted;
2049
+ if (Object.prototype.hasOwnProperty.call(decoded, 'group_id')) {
2050
+ const decryptedGroupId = decoded.group_id;
2051
+ if (typeof decryptedGroupId !== 'string' || !decryptedGroupId.trim() || decryptedGroupId.trim() !== groupId)
2052
+ return null;
2053
+ }
2054
+ return normalizeGroupMentionMode({
2055
+ ...decoded,
2056
+ group_id: groupId,
2057
+ group_aid: groupAid || groupId,
2058
+ }, row, row.envelope_json);
2059
+ }
2060
+ if (this.recallEventFromGroupMessage(row))
2061
+ return null;
2062
+ const payload = row.payload;
2063
+ if (payload === undefined || payload === null)
2064
+ return null;
2065
+ const payloadType = isJsonObject(payload)
2066
+ ? String(payload.type ?? '').trim()
2067
+ : '';
2068
+ if (payloadType === 'e2ee.encrypted' || payloadType === 'e2ee.group_encrypted')
2069
+ return null;
2070
+ const seq = this.inlineSafePositiveSeq(row.seq);
2071
+ if (seq === null)
2072
+ return null;
2073
+ const result = {
2074
+ message_id: String(row.message_id ?? ''),
2075
+ from: String(row.from_aid ?? ''),
2076
+ group_id: groupId,
2077
+ group_aid: groupAid || groupId,
2078
+ seq,
2079
+ type: String(row.type ?? ''),
2080
+ timestamp: row.t_server,
2081
+ payload: payload,
2082
+ encrypted: false,
2083
+ };
2084
+ if (row.sender_device_id !== undefined)
2085
+ result.sender_device_id = row.sender_device_id;
2086
+ return normalizeGroupMentionMode(result, row, row.envelope_json);
2087
+ }
2088
+ decodedInlineMatches(head, decoded, pushSeq, groupId = '') {
2089
+ if (this.inlineSafePositiveSeq(decoded.seq) !== pushSeq)
2090
+ return false;
2091
+ if (!this.inlineRequiredBinding(head, ['message_id'], decoded, ['message_id']))
2092
+ return false;
2093
+ const headSender = this.consistentBindingValue(head, ['from_aid', 'sender_aid', 'from']);
2094
+ const decodedSender = this.consistentBindingValue(decoded, ['from', 'from_aid', 'sender_aid']);
2095
+ if (!headSender || decodedSender !== headSender)
2096
+ return false;
2097
+ if (groupId)
2098
+ return this.consistentBindingValue(decoded, ['group_id']) === groupId;
2099
+ const headTo = this.consistentBindingValue(head, ['to_aid', 'to']);
2100
+ const decodedTo = this.consistentBindingValue(decoded, ['to', 'to_aid']);
2101
+ return Boolean(headTo) && decodedTo === headTo;
2102
+ }
2103
+ async publishInlineMessage(event, ns, seq, payload, generation) {
2104
+ if (!this.inlineGenerationIsCurrent(generation))
2105
+ return false;
2106
+ const client = this.runtime.client;
2107
+ const tracker = client._seqTracker;
2108
+ const snapshot = typeof tracker.snapshotNamespace === 'function' ? tracker.snapshotNamespace(ns) : null;
2109
+ let delivered = false;
2110
+ try {
2111
+ const published = client._publishAppEvent(event, payload, 'inline-push');
2112
+ if (isPromiseLike(published))
2113
+ await published;
2114
+ if (!this.inlineGenerationIsCurrent(generation))
2115
+ return false;
2116
+ delivered = true;
2117
+ if (!this.inlineGenerationIsCurrent(generation))
2118
+ return false;
2119
+ const needsPull = Boolean(tracker.onMessageSeq(ns, seq));
2120
+ if (!this.inlineGenerationIsCurrent(generation))
2121
+ return false;
2122
+ tracker.commitTailWindow(ns, {
2123
+ windowStartSeq: seq,
2124
+ headSeq: seq,
2125
+ coveredThroughSeq: seq,
2126
+ pageLimit: 1,
2127
+ });
2128
+ const committed = this.syncState(ns);
2129
+ if (needsPull || committed.ack !== seq || committed.tail !== seq || committed.head !== seq) {
2130
+ throw new Error('inline realtime cursor did not collapse after consume');
2131
+ }
2132
+ if (!this.inlineGenerationIsCurrent(generation))
2133
+ return false;
2134
+ client._commitSeqTrackerState(ns);
2135
+ if (!this.inlineGenerationIsCurrent(generation))
2136
+ return false;
2137
+ this.markPublishedSeq(ns, seq);
2138
+ return true;
2139
+ }
2140
+ catch (exc) {
2141
+ if (this.inlineGenerationIsCurrent(generation)
2142
+ && typeof tracker.restoreNamespaceSnapshot === 'function')
2143
+ tracker.restoreNamespaceSnapshot(ns, snapshot);
2144
+ if (this.inlineGenerationIsCurrent(generation) && delivered)
2145
+ this.markPublishedSeq(ns, seq);
2146
+ client._clientLog.debug(`inline realtime consume rolled back: ns=${ns} seq=${seq} err=${formatDeliveryError(exc)}`);
2147
+ return false;
2148
+ }
2149
+ }
2150
+ async tryConsumeInlineP2P(head, ns, pushSeq, expectedVersion, inlineGeneration) {
2151
+ const client = this.runtime.client;
2152
+ if (!isJsonObject(head.inline_message))
2153
+ return false;
2154
+ const row = { ...head.inline_message };
2155
+ if (expectedVersion !== undefined && this.inlineRowKind(row) !== expectedVersion)
2156
+ return false;
2157
+ const runner = client._tryRunInlineRealtime;
2158
+ if (typeof runner !== 'function')
2159
+ return false;
2160
+ const generation = inlineGeneration ?? this.inlineAckGeneration;
2161
+ if (!this.inlineGenerationIsCurrent(generation))
2162
+ return false;
2163
+ const result = await runner.call(client, ns, pushSeq, async () => {
2164
+ if (!this.inlineGenerationIsCurrent(generation) || !this.inlineStateIsCaughtUp(ns, pushSeq) || !this.inlineHeadMatches(head, row, pushSeq)
2165
+ || !this.reservePendingP2PInlineAck(ns, pushSeq, row))
2166
+ return false;
2167
+ let committed = false;
2168
+ try {
2169
+ const decoded = await this.decodeInlineP2P(row);
2170
+ if (!this.inlineGenerationIsCurrent(generation) || !decoded || !this.inlineStateIsCaughtUp(ns, pushSeq)
2171
+ || !this.decodedInlineMatches(head, decoded, pushSeq))
2172
+ return false;
2173
+ const appEvent = this.p2pAppEventForMessage(decoded);
2174
+ const published = await this.publishInlineMessage(appEvent.event, ns, pushSeq, appEvent.payload, generation);
2175
+ if (!this.inlineGenerationIsCurrent(generation) || !published)
2176
+ return false;
2177
+ committed = this.markPendingP2PInlineAckCommitted(ns, pushSeq);
2178
+ return committed;
2179
+ }
2180
+ finally {
2181
+ if (!committed && this.inlineGenerationIsCurrent(generation))
2182
+ this.dropPendingP2PInlineAck(ns, pushSeq);
1325
2183
  }
1326
- client._gapFillDone.set(dedupKey, Date.now());
2184
+ });
2185
+ return this.inlineGenerationIsCurrent(generation) && result?.acquired === true && result.value === true;
2186
+ }
2187
+ async tryConsumeInlineGroup(head, groupId, ns, pushSeq, expectedVersion, inlineGeneration) {
2188
+ const client = this.runtime.client;
2189
+ if (!isJsonObject(head.inline_message))
2190
+ return false;
2191
+ const row = { ...head.inline_message };
2192
+ if (expectedVersion !== undefined && this.inlineRowKind(row) !== expectedVersion)
2193
+ return false;
2194
+ const runner = client._tryRunInlineRealtime;
2195
+ if (typeof runner !== 'function')
2196
+ return false;
2197
+ const groupAid = typeof head.group_aid === 'string' ? head.group_aid.trim() : '';
2198
+ const generation = inlineGeneration ?? this.inlineAckGeneration;
2199
+ if (!this.inlineGenerationIsCurrent(generation))
2200
+ return false;
2201
+ const result = await runner.call(client, ns, pushSeq, async () => {
2202
+ if (!this.inlineGenerationIsCurrent(generation) || !this.inlineStateIsCaughtUp(ns, pushSeq) || !this.inlineHeadMatches(head, row, pushSeq, groupId)
2203
+ || !this.reservePendingGroupInlineAck(ns, pushSeq, row, groupAid))
2204
+ return false;
2205
+ let committed = false;
1327
2206
  try {
1328
- const pulled = await client._pullV2(0, 50, { gateLocked: true });
1329
- const newContig = client._seqTracker.getContiguousSeq(ns);
1330
- client._clientLog.debug(`_onV2PushNotification pull done: contiguous_seq=${contigBefore}->${newContig} (push_seq=${pushSeq || 'null'})`);
1331
- if (newContig <= operationBefore)
1332
- return 0;
1333
- return pulled.length;
2207
+ const decoded = await this.decodeInlineGroup(row, groupId, groupAid || groupId);
2208
+ if (!this.inlineGenerationIsCurrent(generation) || !decoded || !this.inlineStateIsCaughtUp(ns, pushSeq)
2209
+ || !this.decodedInlineMatches(head, decoded, pushSeq, groupId))
2210
+ return false;
2211
+ const published = await this.publishInlineMessage('group.message_created', ns, pushSeq, decoded, generation);
2212
+ if (!this.inlineGenerationIsCurrent(generation) || !published)
2213
+ return false;
2214
+ committed = this.markPendingGroupInlineAckCommitted(ns, pushSeq);
2215
+ return committed;
1334
2216
  }
1335
2217
  finally {
1336
- client._gapFillDone.delete(dedupKey);
2218
+ if (!committed && this.inlineGenerationIsCurrent(generation))
2219
+ this.dropPendingGroupInlineAck(ns, pushSeq);
1337
2220
  }
1338
- }, true, () => client._recordPendingP2pPull(ns, pushSeq)).catch((exc) => {
1339
- const newContig = client._seqTracker.getContiguousSeq(ns);
1340
- client._clientLog.warn(`V2 push auto-pull failed: contiguous_seq=${contigBefore}->${newContig} err=${formatDeliveryError(exc)}`);
1341
2221
  });
2222
+ return this.inlineGenerationIsCurrent(generation) && result?.acquired === true && result.value === true;
2223
+ }
2224
+ reservePendingP2PInlineAck(ns, seq, row) {
2225
+ if (!ns || !Number.isSafeInteger(seq) || seq <= 0 || this.pendingP2PInlineAcks?.has(ns)
2226
+ || this.pendingInlineAckNamespaceCount() >= MAX_INLINE_PENDING_ACK_NAMESPACES)
2227
+ return false;
2228
+ const pendingMap = this.pendingP2PInlineAcks ?? new Map();
2229
+ this.pendingP2PInlineAcks = pendingMap;
2230
+ pendingMap.set(ns, {
2231
+ seq,
2232
+ row: this.inlinePostAckRow(row),
2233
+ acking: false,
2234
+ committed: false,
2235
+ observedAckSeq: 0,
2236
+ });
2237
+ return true;
2238
+ }
2239
+ dropPendingP2PInlineAck(ns, seq) {
2240
+ const pending = this.pendingP2PInlineAcks?.get(ns);
2241
+ if (pending?.seq === seq)
2242
+ this.pendingP2PInlineAcks?.delete(ns);
2243
+ }
2244
+ completePendingP2PInlineAck(ns, seq) {
2245
+ if (!ns || !Number.isSafeInteger(seq) || seq <= 0)
2246
+ return;
2247
+ const pending = this.pendingP2PInlineAcks?.get(ns);
2248
+ if (!pending || seq < pending.seq)
2249
+ return;
2250
+ if (!pending.committed) {
2251
+ pending.observedAckSeq = Math.max(pending.observedAckSeq, seq);
2252
+ return;
2253
+ }
2254
+ this.pendingP2PInlineAcks?.delete(ns);
2255
+ this.runPostInlineCommit(pending.row);
2256
+ }
2257
+ markPendingP2PInlineAckCommitted(ns, seq) {
2258
+ const pending = this.pendingP2PInlineAcks?.get(ns);
2259
+ if (!pending || pending.seq !== seq)
2260
+ return false;
2261
+ pending.committed = true;
2262
+ if (pending.observedAckSeq < seq)
2263
+ return true;
2264
+ this.pendingP2PInlineAcks?.delete(ns);
2265
+ this.runPostInlineCommit(pending.row);
2266
+ return true;
2267
+ }
2268
+ retryPendingP2PInlineAck(ns, seq, reason, ackThrough = seq) {
2269
+ const pending = this.pendingP2PInlineAcks?.get(ns);
2270
+ if (!pending || pending.seq !== seq)
2271
+ return false;
2272
+ if (pending.acking || !pending.committed)
2273
+ return true;
2274
+ pending.acking = true;
2275
+ const requestedSeq = Math.max(seq, ackThrough);
2276
+ const generation = this.inlineAckGeneration;
2277
+ this.runtime.client._withBackgroundRpc(() => this.runtime.client._ackV2(requestedSeq))
2278
+ .then((result) => {
2279
+ this.onP2PV2AckResult(result, requestedSeq, generation);
2280
+ }).catch((error) => {
2281
+ const current = this.pendingP2PInlineAcks?.get(ns);
2282
+ if (current === pending)
2283
+ current.acking = false;
2284
+ this.runtime.client._clientLog.debug(`message.v2 ${reason} ack failed: ${formatDeliveryError(error)}`);
2285
+ }).finally(() => {
2286
+ const current = this.pendingP2PInlineAcks?.get(ns);
2287
+ if (current === pending)
2288
+ current.acking = false;
2289
+ });
2290
+ return true;
2291
+ }
2292
+ onP2PV2AckSucceeded(upToSeq, generation = this.inlineAckGeneration) {
2293
+ if (!this.inlineGenerationIsCurrent(generation))
2294
+ return;
2295
+ const ns = this.runtime.client._aid ? `p2p:${this.runtime.client._aid}` : '';
2296
+ if (!ns || !Number.isSafeInteger(upToSeq) || upToSeq <= 0)
2297
+ return;
2298
+ this.completePendingP2PInlineAck(ns, upToSeq);
2299
+ }
2300
+ onP2PV2AckResult(result, requestedSeq, generation = this.inlineAckGeneration) {
2301
+ if (!this.pendingP2PInlineAcks?.size || !this.inlineGenerationIsCurrent(generation))
2302
+ return;
2303
+ this.onP2PV2AckSucceeded(this.resolveActualAckSeq(result, requestedSeq), generation);
2304
+ }
2305
+ onP2PV2PullAckResult(result, requestedSeq, generation) {
2306
+ if (!this.pendingP2PInlineAcks?.size || !this.inlineGenerationIsCurrent(generation)
2307
+ || !Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
2308
+ return;
2309
+ let actualAckSeq = requestedSeq;
2310
+ const response = isJsonObject(result) ? result : null;
2311
+ if (response && Object.prototype.hasOwnProperty.call(response, 'server_ack_seq')) {
2312
+ const raw = response.server_ack_seq;
2313
+ actualAckSeq = typeof raw === 'number' && Number.isSafeInteger(raw) && raw >= 0 ? raw : 0;
2314
+ }
2315
+ this.onP2PV2AckSucceeded(actualAckSeq, generation);
2316
+ }
2317
+ async onV2PushNotification(data, expectedVersion, inlineGeneration) {
2318
+ const client = this.runtime.client;
2319
+ if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
2320
+ return;
2321
+ if (!client._v2Session)
2322
+ return;
2323
+ const pushData = isJsonObject(data) ? data : null;
2324
+ const rawPushSeq = pushData?.seq;
2325
+ const ns = client._aid ? `p2p:${client._aid}` : '';
2326
+ if (!pushData || !ns)
2327
+ return;
2328
+ const inlinePresent = Object.prototype.hasOwnProperty.call(pushData, 'inline_message');
2329
+ const pushSeq = this.realtimeHeadSeq(rawPushSeq, inlinePresent);
2330
+ if (pushSeq === null) {
2331
+ if (inlinePresent && rawPushSeq !== undefined && rawPushSeq !== null) {
2332
+ await this.recoverInvalidP2PRealtimeHead(ns, rawPushSeq);
2333
+ }
2334
+ return;
2335
+ }
2336
+ let inlineConsumed = false;
2337
+ const pendingInlineAck = this.pendingP2PInlineAcks?.get(ns);
2338
+ const inlineBlockedByPendingAck = Boolean(pendingInlineAck && pendingInlineAck.seq !== pushSeq);
2339
+ if (inlinePresent && !inlineBlockedByPendingAck && isJsonObject(pushData.inline_message))
2340
+ try {
2341
+ inlineConsumed = await this.tryConsumeInlineP2P(pushData, ns, pushSeq, expectedVersion, inlineGeneration);
2342
+ if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
2343
+ return;
2344
+ if (inlineConsumed) {
2345
+ client._clientLog.debug(`V2 P2P inline push consumed: seq=${pushSeq}`);
2346
+ this.retryPendingP2PInlineAck(ns, pushSeq, 'inline push');
2347
+ return;
2348
+ }
2349
+ }
2350
+ catch (exc) {
2351
+ if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
2352
+ return;
2353
+ client._clientLog.warn(`V2 P2P inline push rejected, falling back to Tail: seq=${pushSeq} err=${formatDeliveryError(exc)}`);
2354
+ }
2355
+ const before = this.syncState(ns);
2356
+ client._seqTracker.updateMaxSeen(ns, pushSeq);
2357
+ const hasGap = before.ack < before.tail - 1;
2358
+ if (pushSeq <= before.head && !hasGap && !inlinePresent) {
2359
+ client._clientLog.debug(`V2 P2P duplicate push ignored: seq=${pushSeq} A/T/H=${JSON.stringify(before)}`);
2360
+ this.retryPendingP2PInlineAck(ns, pushSeq, 'covered inline push');
2361
+ return;
2362
+ }
2363
+ if (!this.beginRealtimeSync(ns)) {
2364
+ client._clientLog.debug(`V2 P2P push coalesced into in-flight realtime sync: ns=${ns} seq=${pushSeq}`);
2365
+ return;
2366
+ }
2367
+ try {
2368
+ const syncResult = await this.runP2PRealtimeSync(ns, pushSeq, 50, inlinePresent && !inlineConsumed);
2369
+ if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
2370
+ return;
2371
+ const tailHead = this.realtimeTailHead(syncResult);
2372
+ const maxSeen = Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? 0);
2373
+ if (client._sessionOptions?.background_sync !== false
2374
+ && tailHead !== null && Number.isSafeInteger(maxSeen) && maxSeen > tailHead) {
2375
+ await this.runP2PForwardRecovery(ns, 50).catch((exc) => {
2376
+ client._clientLog.warn(`V2 P2P stale Tail Forward recovery failed: ${formatDeliveryError(exc)}`);
2377
+ });
2378
+ if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
2379
+ return;
2380
+ }
2381
+ }
2382
+ catch (exc) {
2383
+ client._clientLog.warn(`V2 P2P push auto-sync failed: ${formatDeliveryError(exc)}`);
2384
+ }
2385
+ finally {
2386
+ if (inlineGeneration === undefined || this.inlineGenerationIsCurrent(inlineGeneration)) {
2387
+ this.endRealtimeSync(ns);
2388
+ }
2389
+ }
1342
2390
  }
1343
2391
  enqueueOnlineUnreadHint(data) {
1344
2392
  const client = this.runtime.client;
@@ -1359,12 +2407,11 @@ export class MessageDeliveryEngine {
1359
2407
  if (client._onlineUnreadHintDrainActive)
1360
2408
  return;
1361
2409
  this.runtime.delivery.setOnlineUnreadHintDrainActive(true);
2410
+ const attemptedAckByGroup = new Map();
1362
2411
  try {
1363
2412
  while (client._onlineUnreadHintQueue.size > 0) {
1364
2413
  if (client.state !== ConnectionState.READY)
1365
2414
  return;
1366
- if (client._sessionOptions?.background_sync === false)
1367
- return;
1368
2415
  const groupId = client._onlineUnreadHintQueue.keys().next().value;
1369
2416
  if (!groupId)
1370
2417
  return;
@@ -1372,6 +2419,11 @@ export class MessageDeliveryEngine {
1372
2419
  client._onlineUnreadHintQueue.delete(groupId);
1373
2420
  if (!payload)
1374
2421
  continue;
2422
+ const currentAck = this.syncState(`group:${groupId}`).ack;
2423
+ const attemptedAck = attemptedAckByGroup.get(groupId);
2424
+ if (attemptedAck !== undefined && currentAck <= attemptedAck)
2425
+ continue;
2426
+ attemptedAckByGroup.set(groupId, currentAck);
1375
2427
  await this.onRawGroupV2MessageCreated({ ...payload, _online_hint_drained: true });
1376
2428
  const intervalMs = Math.max(0, Number(client._onlineUnreadHintIntervalMs ?? 50) || 0);
1377
2429
  if (intervalMs > 0 && client._onlineUnreadHintQueue.size > 0) {
@@ -1386,62 +2438,110 @@ export class MessageDeliveryEngine {
1386
2438
  this.runtime.delivery.setOnlineUnreadHintDrainActive(false);
1387
2439
  }
1388
2440
  }
1389
- async onRawGroupV2MessageCreated(data) {
2441
+ async onRawGroupV2MessageCreated(data, expectedVersion, inlineGeneration) {
1390
2442
  const client = this.runtime.client;
2443
+ if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
2444
+ return;
1391
2445
  if (!isJsonObject(data) || !client._v2Session) {
1392
2446
  client._clientLog.debug(`_onRawGroupV2MessageCreated skipped: is_object=${String(isJsonObject(data))}, has_v2_session=${String(!!client._v2Session)}`);
1393
2447
  return;
1394
2448
  }
1395
2449
  const d = data;
1396
2450
  client._logMessageDebug('server-push', '_raw.group.v2.message_created', 'group.message_created', d);
1397
- const rawGroupId = String(d.group_id ?? '').trim();
1398
- const groupId = normalizeGroupId(rawGroupId) || rawGroupId;
1399
- const seq = Number(d.seq ?? 0);
1400
- if (!groupId || !Number.isFinite(seq) || seq <= 0) {
2451
+ const groupId = typeof d.group_id === 'string' ? d.group_id.trim() : '';
2452
+ const rawSeq = d.seq;
2453
+ const inlinePresent = Object.prototype.hasOwnProperty.call(d, 'inline_message');
2454
+ const seq = this.realtimeHeadSeq(rawSeq, inlinePresent);
2455
+ if (!groupId) {
1401
2456
  client._clientLog.debug(`_onRawGroupV2MessageCreated skipped: group=${groupId || '<empty>'}, seq=${String(d.seq ?? '')}`);
1402
2457
  return;
1403
2458
  }
2459
+ if (seq === null) {
2460
+ if (inlinePresent && rawSeq !== undefined && rawSeq !== null) {
2461
+ await this.recoverInvalidGroupRealtimeHead(groupId, `group:${groupId}`, rawSeq);
2462
+ }
2463
+ return;
2464
+ }
1404
2465
  const eventKind = String(d.kind ?? '').trim();
1405
- if (eventKind === 'group.online_unread_hint' && !d._online_hint_drained) {
1406
- if (client._sessionOptions?.background_sync === false) {
1407
- client._clientLog.debug(`_onRawGroupV2MessageCreated skipped online unread hint: group=${groupId} background_sync=false`);
2466
+ if (eventKind === 'group.online_unread_hint') {
2467
+ if (!d._online_hint_drained) {
2468
+ if (client._sessionOptions.background_sync === false)
2469
+ return;
2470
+ this.enqueueOnlineUnreadHint(d);
1408
2471
  return;
1409
2472
  }
1410
- this.enqueueOnlineUnreadHint(d);
2473
+ // Hint 不是真实消息 Head:不消费 inline、不推进水位、也不直接 ACK。
2474
+ // 只在后台同步开启时,从已提交 Ack 强制做一次普通 Forward,绝不进入 Tail planner。
2475
+ if (client._sessionOptions.background_sync === false)
2476
+ return;
2477
+ try {
2478
+ await this.runGroupForwardRecovery(groupId, `group:${groupId}`, 50);
2479
+ }
2480
+ catch (exc) {
2481
+ client._clientLog.warn(`group online unread hint recovery pull failed: group=${groupId} err=${formatDeliveryError(exc)}`);
2482
+ }
1411
2483
  return;
1412
2484
  }
1413
2485
  const ns = `group:${groupId}`;
2486
+ let inlineConsumed = false;
2487
+ const pendingInlineAck = this.pendingGroupInlineAcks?.get(ns);
2488
+ const inlineBlockedByPendingAck = Boolean(pendingInlineAck && pendingInlineAck.seq !== seq);
2489
+ if (inlinePresent && !inlineBlockedByPendingAck && isJsonObject(d.inline_message)) {
2490
+ try {
2491
+ inlineConsumed = await this.tryConsumeInlineGroup(d, groupId, ns, seq, expectedVersion, inlineGeneration);
2492
+ if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
2493
+ return;
2494
+ if (inlineConsumed) {
2495
+ client._clientLog.debug(`V2 group inline push consumed: group=${groupId} seq=${seq}`);
2496
+ this.retryPendingGroupInlineAck(ns, seq, 'inline push');
2497
+ return;
2498
+ }
2499
+ }
2500
+ catch (exc) {
2501
+ if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
2502
+ return;
2503
+ client._clientLog.warn(`V2 group inline push rejected, falling back to Tail: group=${groupId} seq=${seq} err=${formatDeliveryError(exc)}`);
2504
+ }
2505
+ }
2506
+ const before = this.syncState(ns);
1414
2507
  client._seqTracker.updateMaxSeen(ns, seq);
1415
- const contigBefore = client._seqTracker.getContiguousSeq(ns);
1416
- client._clientLog.debug(`_onRawGroupV2MessageCreated enter: group=${groupId}, seq=${seq}, contiguous=${contigBefore}, max_seen=${client._seqTracker.getMaxSeenSeq(ns)}`);
1417
- if (contigBefore === seq || (eventKind === 'group.online_unread_hint' && contigBefore > seq)) {
1418
- client._clientLog.debug(`_onRawGroupV2MessageCreated duplicate push already covered: group=${groupId} seq=${seq}`);
1419
- this.fireGroupV2Ack(groupId, seq, 'covered push');
2508
+ const hasGap = before.ack < before.tail - 1;
2509
+ if (seq <= before.head && !hasGap && !inlinePresent) {
2510
+ client._clientLog.debug(`V2 group duplicate push ignored: group=${groupId} seq=${seq} A/T/H=${JSON.stringify(before)}`);
2511
+ const pending = this.pendingGroupInlineAcks?.get(ns);
2512
+ if (pending?.seq === seq) {
2513
+ this.retryPendingGroupInlineAck(ns, seq, 'covered inline push');
2514
+ }
2515
+ else {
2516
+ this.fireGroupV2Ack(groupId, seq, 'covered push');
2517
+ }
1420
2518
  return;
1421
2519
  }
1422
- const afterSeq = client._repairPushContiguousBound(ns, seq, false, '_raw.group.v2.message_created');
1423
- const dedupKey = `v2_group_push:${groupId}:${afterSeq}`;
1424
- const pullTask = client._tryRunBackgroundPull(ns, async () => {
1425
- const pullAfterSeq = client._seqTracker.getContiguousSeq(ns);
1426
- if (client._gapFillDone.has(dedupKey)) {
1427
- client._clientLog.debug(`_onRawGroupV2MessageCreated skipped duplicate in-flight pull: group=${groupId}, dedup=${dedupKey}`);
1428
- return 0;
1429
- }
1430
- client._gapFillDone.set(dedupKey, Date.now());
1431
- try {
1432
- client._clientLog.debug(`_onRawGroupV2MessageCreated auto-pull start: group=${groupId}, after_seq=${pullAfterSeq}, push_seq=${seq}`);
1433
- const pulled = await client._pullGroupV2(groupId, pullAfterSeq, 50, { gateLocked: true });
1434
- const newContig = client._seqTracker.getContiguousSeq(ns);
1435
- client._clientLog.debug(`_onRawGroupV2MessageCreated auto-pull done: group=${groupId}, after_seq=${pullAfterSeq}, push_seq=${seq}, contiguous=${newContig}`);
1436
- if (newContig <= pullAfterSeq)
1437
- return 0;
1438
- return pulled.length;
2520
+ if (!this.beginRealtimeSync(ns)) {
2521
+ client._clientLog.debug(`V2 group push coalesced into in-flight realtime sync: ns=${ns} seq=${seq}`);
2522
+ return;
2523
+ }
2524
+ const pullTask = this.runGroupRealtimeSync(groupId, ns, seq, 50, inlinePresent && !inlineConsumed).then(async (syncResult) => {
2525
+ if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
2526
+ return;
2527
+ const tailHead = this.realtimeTailHead(syncResult);
2528
+ const maxSeen = Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? 0);
2529
+ if (client._sessionOptions?.background_sync !== false
2530
+ && tailHead !== null && Number.isSafeInteger(maxSeen) && maxSeen > tailHead) {
2531
+ await this.runGroupForwardRecovery(groupId, ns, 50).catch((exc) => {
2532
+ client._clientLog.warn(`V2 group stale Tail Forward recovery failed: group=${groupId} err=${formatDeliveryError(exc)}`);
2533
+ });
2534
+ if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
2535
+ return;
1439
2536
  }
1440
- finally {
1441
- client._gapFillDone.delete(dedupKey);
2537
+ }).catch((exc) => {
2538
+ if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
2539
+ return;
2540
+ client._clientLog.warn(`V2 group push auto-sync failed: group=${groupId} err=${formatDeliveryError(exc)}`);
2541
+ }).finally(() => {
2542
+ if (inlineGeneration === undefined || this.inlineGenerationIsCurrent(inlineGeneration)) {
2543
+ this.endRealtimeSync(ns);
1442
2544
  }
1443
- }, true).catch((exc) => {
1444
- client._clientLog.warn(`V2 group push auto-pull failed: group=${groupId} err=${formatDeliveryError(exc)}`);
1445
2545
  });
1446
2546
  if (d._online_hint_drained) {
1447
2547
  await pullTask;
@@ -1452,6 +2552,15 @@ export class MessageDeliveryEngine {
1452
2552
  if (!client._aid)
1453
2553
  return;
1454
2554
  try {
2555
+ const loadSync = client._tokenStore.loadAllSyncStates;
2556
+ if (typeof loadSync === 'function' && typeof client._seqTracker.restoreSyncState === 'function') {
2557
+ let state = loadSync.call(client._tokenStore, client._aid, client._deviceId, client._slotId);
2558
+ if (state && Object.keys(state).length > 0) {
2559
+ state = this.migrateSeqStateGroupIds(state);
2560
+ client._seqTracker.restoreSyncState(state);
2561
+ return;
2562
+ }
2563
+ }
1455
2564
  const loadAll = client._tokenStore.loadAllSeqs;
1456
2565
  if (typeof loadAll === 'function') {
1457
2566
  let state = loadAll.call(client._tokenStore, client._aid, client._deviceId, client._slotId);
@@ -1467,7 +2576,10 @@ export class MessageDeliveryEngine {
1467
2576
  if (instanceState && typeof instanceState.seq_tracker_state === 'object') {
1468
2577
  let state = instanceState.seq_tracker_state;
1469
2578
  state = this.migrateSeqStateGroupIds(state);
1470
- client._seqTracker.restoreState(state);
2579
+ if (typeof client._seqTracker.restoreSyncState === 'function')
2580
+ client._seqTracker.restoreSyncState(state);
2581
+ else
2582
+ client._seqTracker.restoreState(state);
1471
2583
  }
1472
2584
  }
1473
2585
  }
@@ -1484,53 +2596,7 @@ export class MessageDeliveryEngine {
1484
2596
  }
1485
2597
  }
1486
2598
  migrateSeqStateGroupIds(state) {
1487
- const client = this.runtime.client;
1488
- if (!state || Object.keys(state).length === 0)
1489
- return state;
1490
- const renameMap = {};
1491
- for (const ns of Object.keys(state)) {
1492
- for (const prefix of ['group_event:', 'group_msg:']) {
1493
- if (ns.startsWith(prefix)) {
1494
- const oldGid = ns.slice(prefix.length);
1495
- const newGid = normalizeGroupId(oldGid);
1496
- if (newGid && newGid !== oldGid) {
1497
- renameMap[ns] = `${prefix}${newGid}`;
1498
- }
1499
- break;
1500
- }
1501
- }
1502
- }
1503
- if (Object.keys(renameMap).length === 0)
1504
- return state;
1505
- const newState = { ...state };
1506
- for (const [oldNs, newNs] of Object.entries(renameMap)) {
1507
- const oldVal = Number(newState[oldNs] ?? 0);
1508
- const curVal = Number(newState[newNs] ?? 0);
1509
- delete newState[oldNs];
1510
- newState[newNs] = Math.max(oldVal, curVal);
1511
- }
1512
- client._clientLog.info(`SeqTracker group_id migration: ${Object.keys(renameMap).length} namespaces rewritten`);
1513
- const saver = client._tokenStore.saveSeq;
1514
- const deleter = client._tokenStore.deleteSeq;
1515
- if (typeof saver === 'function' && client._aid) {
1516
- for (const [oldNs, newNs] of Object.entries(renameMap)) {
1517
- if (typeof deleter === 'function') {
1518
- try {
1519
- deleter.call(client._tokenStore, client._aid, client._deviceId, client._slotId, oldNs);
1520
- }
1521
- catch (e) {
1522
- client._clientLog.debug(`delete old seq ns failed: ns=${oldNs} err=${formatDeliveryError(e)}`);
1523
- }
1524
- }
1525
- try {
1526
- saver.call(client._tokenStore, client._aid, client._deviceId, client._slotId, newNs, newState[newNs]);
1527
- }
1528
- catch (e) {
1529
- client._clientLog.debug(`write new seq ns failed: ns=${newNs} err=${formatDeliveryError(e)}`);
1530
- }
1531
- }
1532
- }
1533
- return newState;
2599
+ return state;
1534
2600
  }
1535
2601
  saveSeqTrackerState() {
1536
2602
  const client = this.runtime.client;
@@ -1539,7 +2605,9 @@ export class MessageDeliveryEngine {
1539
2605
  this.flushSeqTrackerPending();
1540
2606
  return;
1541
2607
  }
1542
- const state = client._seqTracker.exportState();
2608
+ const state = typeof client._seqTracker.exportSyncState === 'function'
2609
+ ? client._seqTracker.exportSyncState()
2610
+ : Object.fromEntries(Object.entries(client._seqTracker.exportState()).map(([ns, seq]) => [ns, { ack: Number(seq), tail: Number(seq), head: Number(seq) }]));
1543
2611
  const pending = this.seqTrackerPending();
1544
2612
  if (pending.size > 0 && client._seqTrackerPendingPersistContext === context) {
1545
2613
  const liveNamespaces = new Set(Object.keys(state));
@@ -1567,9 +2635,27 @@ export class MessageDeliveryEngine {
1567
2635
  if (forceSeq !== undefined) {
1568
2636
  client._seqTracker.forceContiguousSeq(ns, seq);
1569
2637
  }
1570
- this.mergeSeqTrackerPending(context, { [ns]: seq });
2638
+ const state = typeof client._seqTracker.getSyncState === 'function'
2639
+ ? client._seqTracker.getSyncState(ns)
2640
+ : { ack: seq, tail: seq, head: seq };
2641
+ this.mergeSeqTrackerPending(context, { [ns]: state });
1571
2642
  this.scheduleSeqTrackerFlush();
1572
2643
  }
2644
+ commitSeqTrackerState(ns) {
2645
+ const client = this.runtime.client;
2646
+ const context = this.currentSeqTrackerContext();
2647
+ if (!context || !ns)
2648
+ return;
2649
+ const state = typeof client._seqTracker.getSyncState === 'function'
2650
+ ? client._seqTracker.getSyncState(ns)
2651
+ : { ack: client._seqTracker.getContiguousSeq(ns), tail: client._seqTracker.getContiguousSeq(ns), head: client._seqTracker.getContiguousSeq(ns) };
2652
+ if (![state.ack, state.tail, state.head].some((value) => Number(value) > 0))
2653
+ return;
2654
+ const [aid, deviceId, slotId] = JSON.parse(context);
2655
+ this.writeSeqTrackerState(aid, deviceId, slotId, { [ns]: state }, true);
2656
+ this.dropSeqTrackerPending(ns);
2657
+ client._clientLog.debug(`seq_tracker atomic commit: ns=${ns} A/T/H=${JSON.stringify(state)} context=${context}`);
2658
+ }
1573
2659
  flushSeqTrackerPending() {
1574
2660
  const client = this.runtime.client;
1575
2661
  this.cancelSeqTrackerFlushTimer();
@@ -1615,10 +2701,12 @@ export class MessageDeliveryEngine {
1615
2701
  this.flushSeqTrackerPending();
1616
2702
  }
1617
2703
  client._seqTrackerPendingPersistContext = context;
1618
- for (const [ns, seq] of Object.entries(state)) {
1619
- const seqNum = Number(seq);
1620
- if (ns && Number.isFinite(seqNum) && seqNum > 0) {
1621
- pending.set(ns, seqNum);
2704
+ for (const [ns, raw] of Object.entries(state)) {
2705
+ const ack = Number(raw.ack);
2706
+ const tail = Number(raw.tail);
2707
+ const head = Number(raw.head);
2708
+ if (ns && [ack, tail, head].every(Number.isFinite) && Math.max(ack, tail, head) > 0) {
2709
+ pending.set(ns, { ack, tail, head });
1622
2710
  }
1623
2711
  }
1624
2712
  }
@@ -1641,15 +2729,20 @@ export class MessageDeliveryEngine {
1641
2729
  client._seqTrackerFlushTimer = null;
1642
2730
  }
1643
2731
  }
1644
- writeSeqTrackerState(aid, deviceId, slotId, state) {
2732
+ writeSeqTrackerState(aid, deviceId, slotId, state, throwOnError = false) {
1645
2733
  const client = this.runtime.client;
1646
2734
  if (!aid || Object.keys(state).length === 0)
1647
2735
  return;
1648
2736
  try {
2737
+ const saveSync = client._tokenStore.saveSyncStates;
2738
+ if (typeof saveSync === 'function') {
2739
+ saveSync.call(client._tokenStore, aid, deviceId, slotId, state);
2740
+ return;
2741
+ }
1649
2742
  const saveFn = client._tokenStore.saveSeq;
1650
2743
  if (typeof saveFn === 'function') {
1651
- for (const [ns, seq] of Object.entries(state)) {
1652
- saveFn.call(client._tokenStore, aid, deviceId, slotId, ns, seq);
2744
+ for (const [ns, triplet] of Object.entries(state)) {
2745
+ saveFn.call(client._tokenStore, aid, deviceId, slotId, ns, triplet.ack);
1653
2746
  }
1654
2747
  return;
1655
2748
  }
@@ -1674,6 +2767,8 @@ export class MessageDeliveryEngine {
1674
2767
  slot_id: slotId,
1675
2768
  error: String(error),
1676
2769
  }).catch(() => { });
2770
+ if (throwOnError)
2771
+ throw exc;
1677
2772
  }
1678
2773
  }
1679
2774
  persistRepairedSeq(ns) {
@@ -1721,10 +2816,21 @@ export class MessageDeliveryEngine {
1721
2816
  if (next < 0)
1722
2817
  next = 0;
1723
2818
  if (ns) {
1724
- const maxSeen = client._seqTracker.getMaxSeenSeq(ns);
1725
- if (maxSeen > 0 && next > maxSeen) {
1726
- client._clientLog.warn(`ack clamp: method=${method} ${field}=${original} > max_seen=${maxSeen}, clamp`);
1727
- next = maxSeen;
2819
+ const committedAck = client._seqTracker.getContiguousSeq(ns);
2820
+ if (next > committedAck) {
2821
+ client._clientLog.warn(`ack clamp: method=${method} ${field}=${original} > committed_ack=${committedAck}, clamp`);
2822
+ next = committedAck;
2823
+ }
2824
+ if (method === 'message.v2.ack' || method === 'message.ack') {
2825
+ const pending = this.pendingP2PInlineAcks?.get(ns);
2826
+ if (pending && !pending.committed && next >= pending.seq)
2827
+ next = pending.seq - 1;
2828
+ }
2829
+ else if (method === 'group.v2.ack' || method === 'group.ack_messages') {
2830
+ const canonicalNs = this.resolvePendingGroupInlineAckNamespace(ns);
2831
+ const pending = this.pendingGroupInlineAcks?.get(canonicalNs);
2832
+ if (pending && !pending.committed && next >= pending.seq)
2833
+ next = pending.seq - 1;
1728
2834
  }
1729
2835
  }
1730
2836
  return next;
@@ -1747,7 +2853,7 @@ export class MessageDeliveryEngine {
1747
2853
  return clampField('seq', ns);
1748
2854
  }
1749
2855
  if (method === 'group.v2.ack' || method === 'group.ack_messages' || method === 'group.ack_events') {
1750
- const groupId = normalizeGroupId(String(params.group_id ?? '')) || String(params.group_id ?? '').trim();
2856
+ const groupId = String(params.group_id ?? params.group_aid ?? '').trim();
1751
2857
  const ns = groupId ? `group:${groupId}` : '';
1752
2858
  if (method === 'group.v2.ack')
1753
2859
  return clampField('up_to_seq', ns);
@@ -1818,15 +2924,7 @@ export class MessageDeliveryEngine {
1818
2924
  return false;
1819
2925
  }
1820
2926
  const contig = client._seqTracker.getContiguousSeq(ns);
1821
- if (seqNum <= contig) {
1822
- client._clientLog.debug(`publish ordered stale covered: event=${event}, ns=${ns}, seq=${seqNum}, contiguous=${contig}`);
1823
- const queue = client._pendingOrderedMsgs.get(ns);
1824
- queue?.delete(seqNum);
1825
- if (queue && queue.size === 0)
1826
- client._pendingOrderedMsgs.delete(ns);
1827
- return false;
1828
- }
1829
- if (seqNum !== contig + 1) {
2927
+ if (seqNum > contig && seqNum !== contig + 1) {
1830
2928
  client._clientLog.debug(`publish ordered enqueue(gap): event=${event}, ns=${ns}, seq=${seqNum}, contiguous=${contig}`);
1831
2929
  this.enqueueOrderedMessage(ns, event, seqNum, payload);
1832
2930
  return false;
@@ -1898,7 +2996,6 @@ export class MessageDeliveryEngine {
1898
2996
  if (event === 'message.recalled') {
1899
2997
  const recallPublished = await this.publishMessageRecallTombstone(seqNum, payload);
1900
2998
  this.markPublishedSeq(ns, seqNum);
1901
- client._markPulledSeqDelivered(ns, seqNum);
1902
2999
  client._clientLog.debug(`publish pulled delivered: event=${event}, ns=${ns}, seq=${seqNum}`);
1903
3000
  await this.drainOrderedMessages(ns, undefined, true);
1904
3001
  return recallPublished;
@@ -1907,7 +3004,6 @@ export class MessageDeliveryEngine {
1907
3004
  if (isPromiseLike(published))
1908
3005
  await published;
1909
3006
  this.markPublishedSeq(ns, seqNum);
1910
- client._markPulledSeqDelivered(ns, seqNum);
1911
3007
  client._clientLog.debug(`publish pulled delivered: event=${event}, ns=${ns}, seq=${seqNum}`);
1912
3008
  await this.drainOrderedMessages(ns, undefined, true);
1913
3009
  return true;