@agentunion/fastaun-browser 0.5.3 → 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 (151) hide show
  1. package/CHANGELOG.md +130 -56
  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 +130 -56
  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 +87 -41
  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 +307 -267
  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.d.ts +4 -5
  37. package/dist/agent-md.d.ts.map +1 -1
  38. package/dist/agent-md.js +66 -43
  39. package/dist/agent-md.js.map +1 -1
  40. package/dist/aid-store.d.ts +2 -6
  41. package/dist/aid-store.d.ts.map +1 -1
  42. package/dist/aid-store.js +20 -14
  43. package/dist/aid-store.js.map +1 -1
  44. package/dist/auth.d.ts.map +1 -1
  45. package/dist/auth.js +30 -11
  46. package/dist/auth.js.map +1 -1
  47. package/dist/bundle.js +3399 -1069
  48. package/dist/client/delivery.d.ts +75 -8
  49. package/dist/client/delivery.d.ts.map +1 -1
  50. package/dist/client/delivery.js +1419 -245
  51. package/dist/client/delivery.js.map +1 -1
  52. package/dist/client/group-state.d.ts.map +1 -1
  53. package/dist/client/group-state.js +27 -24
  54. package/dist/client/group-state.js.map +1 -1
  55. package/dist/client/lifecycle.d.ts.map +1 -1
  56. package/dist/client/lifecycle.js +48 -4
  57. package/dist/client/lifecycle.js.map +1 -1
  58. package/dist/client/mention-mode.d.ts +7 -0
  59. package/dist/client/mention-mode.d.ts.map +1 -0
  60. package/dist/client/mention-mode.js +184 -0
  61. package/dist/client/mention-mode.js.map +1 -0
  62. package/dist/client/peers.d.ts +1 -1
  63. package/dist/client/peers.d.ts.map +1 -1
  64. package/dist/client/peers.js +26 -3
  65. package/dist/client/peers.js.map +1 -1
  66. package/dist/client/rpc-pipeline.d.ts +22 -1
  67. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  68. package/dist/client/rpc-pipeline.js +309 -74
  69. package/dist/client/rpc-pipeline.js.map +1 -1
  70. package/dist/client/v2-e2ee.d.ts +25 -2
  71. package/dist/client/v2-e2ee.d.ts.map +1 -1
  72. package/dist/client/v2-e2ee.js +606 -97
  73. package/dist/client/v2-e2ee.js.map +1 -1
  74. package/dist/client.d.ts +24 -5
  75. package/dist/client.d.ts.map +1 -1
  76. package/dist/client.js +404 -215
  77. package/dist/client.js.map +1 -1
  78. package/dist/errors.d.ts.map +1 -1
  79. package/dist/errors.js +4 -1
  80. package/dist/errors.js.map +1 -1
  81. package/dist/facades.d.ts +6 -0
  82. package/dist/facades.d.ts.map +1 -1
  83. package/dist/facades.js +199 -104
  84. package/dist/facades.js.map +1 -1
  85. package/dist/group-index.d.ts +6 -1
  86. package/dist/group-index.d.ts.map +1 -1
  87. package/dist/group-index.js +44 -25
  88. package/dist/group-index.js.map +1 -1
  89. package/dist/index.d.ts +1 -1
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +1 -1
  92. package/dist/index.js.map +1 -1
  93. package/dist/keystore/index.d.ts +10 -5
  94. package/dist/keystore/index.d.ts.map +1 -1
  95. package/dist/keystore/indexeddb-identity-store.d.ts +0 -12
  96. package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
  97. package/dist/keystore/indexeddb-identity-store.js +0 -60
  98. package/dist/keystore/indexeddb-identity-store.js.map +1 -1
  99. package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
  100. package/dist/keystore/indexeddb-shared.js +9 -5
  101. package/dist/keystore/indexeddb-shared.js.map +1 -1
  102. package/dist/keystore/indexeddb-token-store.d.ts +3 -1
  103. package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
  104. package/dist/keystore/indexeddb-token-store.js +47 -2
  105. package/dist/keystore/indexeddb-token-store.js.map +1 -1
  106. package/dist/register-flow.d.ts.map +1 -1
  107. package/dist/register-flow.js +28 -3
  108. package/dist/register-flow.js.map +1 -1
  109. package/dist/seq-tracker.d.ts +28 -8
  110. package/dist/seq-tracker.d.ts.map +1 -1
  111. package/dist/seq-tracker.js +218 -61
  112. package/dist/seq-tracker.js.map +1 -1
  113. package/dist/storage/vfs.d.ts +1 -0
  114. package/dist/storage/vfs.d.ts.map +1 -1
  115. package/dist/storage/vfs.js +26 -2
  116. package/dist/storage/vfs.js.map +1 -1
  117. package/dist/tools/cross-sdk-agent.js +376 -10
  118. package/dist/tools/cross-sdk-agent.js.map +1 -1
  119. package/dist/transport.d.ts +12 -0
  120. package/dist/transport.d.ts.map +1 -1
  121. package/dist/transport.js +218 -101
  122. package/dist/transport.js.map +1 -1
  123. package/dist/v2/session/session.d.ts.map +1 -1
  124. package/dist/v2/session/session.js +3 -4
  125. package/dist/v2/session/session.js.map +1 -1
  126. package/dist/v2/state/commitment.d.ts.map +1 -1
  127. package/dist/v2/state/commitment.js +1 -2
  128. package/dist/v2/state/commitment.js.map +1 -1
  129. package/dist/version.d.ts +1 -1
  130. package/dist/version.js +1 -1
  131. package/package.json +6 -5
  132. package/dist/group-resources.d.ts +0 -98
  133. package/dist/group-resources.d.ts.map +0 -1
  134. package/dist/group-resources.js +0 -635
  135. package/dist/group-resources.js.map +0 -1
  136. package/dist/keystore/indexeddb.d.ts +0 -179
  137. package/dist/keystore/indexeddb.d.ts.map +0 -1
  138. package/dist/keystore/indexeddb.js +0 -2031
  139. package/dist/keystore/indexeddb.js.map +0 -1
  140. package/dist/namespaces/auth.d.ts +0 -98
  141. package/dist/namespaces/auth.d.ts.map +0 -1
  142. package/dist/namespaces/auth.js +0 -992
  143. package/dist/namespaces/auth.js.map +0 -1
  144. package/dist/namespaces/custody.d.ts +0 -51
  145. package/dist/namespaces/custody.d.ts.map +0 -1
  146. package/dist/namespaces/custody.js +0 -302
  147. package/dist/namespaces/custody.js.map +0 -1
  148. package/dist/namespaces/meta.d.ts +0 -109
  149. package/dist/namespaces/meta.d.ts.map +0 -1
  150. package/dist/namespaces/meta.js +0 -549
  151. package/dist/namespaces/meta.js.map +0 -1
@@ -1,6 +1,6 @@
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;
@@ -13,7 +13,7 @@ const APP_MESSAGE_ENVELOPE_KEYS = [
13
13
  'module_id', 'message_type', 'type', 'kind', 'version',
14
14
  'from', 'from_aid', 'sender_aid', 'to', 'to_aid', 'group_id',
15
15
  'timestamp', 'created_at', 'encrypted',
16
- 'context', 'protected_headers', 'headers', 'payload_type',
16
+ 'context', 'protected_headers', 'headers', 'payload_type', 'mention_mode',
17
17
  ];
18
18
  const RECALL_PAYLOAD_KEYS = [
19
19
  'message_ids', 'target_message_seqs',
@@ -33,6 +33,7 @@ const APP_GROUP_EVENT_ENVELOPE_KEYS = [
33
33
  'actor_aid', 'sender_aid', 'member_aid', 'target_aid', 'operator_aid',
34
34
  'created_at', 'timestamp', 't_server', 'status', 'device_id', 'slot_id',
35
35
  ];
36
+ const MAX_INLINE_PENDING_ACK_NAMESPACES = 4096;
36
37
  function formatDeliveryError(error) {
37
38
  return error instanceof Error ? error : String(error);
38
39
  }
@@ -47,6 +48,15 @@ function isTransientGapFillError(error) {
47
48
  || text.includes('connection closed')
48
49
  || text.includes('socket closed');
49
50
  }
51
+ function positiveSafeSequenceHint(value) {
52
+ if (typeof value === 'number') {
53
+ return Number.isSafeInteger(value) && value > 0 ? value : 0;
54
+ }
55
+ if (typeof value !== 'string' || !value || value.trim() !== value)
56
+ return 0;
57
+ const parsed = Number(value);
58
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 0;
59
+ }
50
60
  function p2pAppEventFromPlainPullMessage(message) {
51
61
  const payload = message.payload;
52
62
  const payloadType = isJsonObject(payload)
@@ -65,9 +75,85 @@ function p2pAppEventFromPlainPullMessage(message) {
65
75
  }
66
76
  export class MessageDeliveryEngine {
67
77
  runtime;
78
+ realtimeTailResults = null;
79
+ realtimeSyncing = null;
80
+ realtimeAcking = null;
81
+ pendingP2PInlineAcks = null;
82
+ pendingGroupInlineAcks = null;
83
+ pendingGroupInlineAckNamespaces = null;
84
+ // reset 后旧 inline Promise 只能自行结束,不能触碰新身份上下文的状态。
85
+ inlineGeneration = 0;
68
86
  constructor(runtime) {
69
87
  this.runtime = runtime;
70
88
  }
89
+ resetInlineAckState() {
90
+ this.inlineGeneration += 1;
91
+ this.realtimeSyncing = null;
92
+ this.realtimeTailResults = null;
93
+ this.realtimeAcking = null;
94
+ this.pendingP2PInlineAcks = null;
95
+ this.pendingGroupInlineAcks = null;
96
+ this.pendingGroupInlineAckNamespaces = null;
97
+ }
98
+ isInlineGenerationCurrent(generation) {
99
+ return generation === this.inlineGeneration;
100
+ }
101
+ captureInlineGeneration() {
102
+ return this.inlineGeneration;
103
+ }
104
+ beginRealtimeSync(ns) {
105
+ if (!ns || this.realtimeSyncing?.has(ns))
106
+ return false;
107
+ const syncing = this.realtimeSyncing ?? new Set();
108
+ this.realtimeSyncing = syncing;
109
+ syncing.add(ns);
110
+ return true;
111
+ }
112
+ endRealtimeSync(ns) {
113
+ this.realtimeSyncing?.delete(ns);
114
+ }
115
+ beginRealtimeAck(ns, seq) {
116
+ if (!ns || !Number.isSafeInteger(seq) || seq <= 0)
117
+ return false;
118
+ const current = this.realtimeAcking?.get(ns);
119
+ if (current !== undefined) {
120
+ if (seq > current)
121
+ this.realtimeAcking?.set(ns, seq);
122
+ return false;
123
+ }
124
+ const acking = this.realtimeAcking ?? new Map();
125
+ this.realtimeAcking = acking;
126
+ acking.set(ns, seq);
127
+ return true;
128
+ }
129
+ finishRealtimeAck(ns, completedSeq) {
130
+ const latest = this.realtimeAcking?.get(ns) ?? 0;
131
+ this.realtimeAcking?.delete(ns);
132
+ return latest > completedSeq ? latest : 0;
133
+ }
134
+ pendingInlineAckNamespaceCount() {
135
+ return (this.pendingP2PInlineAcks?.size ?? 0) + (this.pendingGroupInlineAcks?.size ?? 0);
136
+ }
137
+ inlinePostAckRow(row) {
138
+ const result = {};
139
+ for (const key of ['envelope_json', 'spk_id', 'group_id', 'message_id']) {
140
+ const value = row[key];
141
+ if (typeof value === 'string' && value)
142
+ result[key] = value;
143
+ }
144
+ if (typeof row.seq === 'number' && Number.isSafeInteger(row.seq) && row.seq > 0)
145
+ result.seq = row.seq;
146
+ return result;
147
+ }
148
+ markRealtimeTailCompleted(result) {
149
+ const completed = this.realtimeTailResults ?? new WeakSet();
150
+ this.realtimeTailResults = completed;
151
+ completed.add(result);
152
+ Object.defineProperty(result, '_realtime_tail_completed', { value: true, enumerable: false });
153
+ }
154
+ isRealtimeTailCompleted(result) {
155
+ return this.realtimeTailResults?.has(result) ?? false;
156
+ }
71
157
  prunePushedSeqs(ns) {
72
158
  const client = this.runtime.client;
73
159
  const pushed = client._pushedSeqs.get(ns);
@@ -106,18 +192,312 @@ export class MessageDeliveryEngine {
106
192
  client._clientLog.warn(`${label} failed: group=${groupId}`, e);
107
193
  });
108
194
  }
109
- fireGroupV2Ack(groupId, ns, seq, label) {
195
+ fireGroupV2Ack(groupId, ns, seq, label, generation = null) {
110
196
  const client = this.runtime.client;
111
197
  const ackSeq = this.clampAckSeq('group.v2.ack', 'up_to_seq', ns, seq);
112
- if (ackSeq <= 0)
198
+ if (ackSeq <= 0 || !this.beginRealtimeAck(ns, ackSeq))
113
199
  return;
114
- void client._callRawV2Rpc('group.v2.ack', {
115
- group_id: groupId,
116
- up_to_seq: ackSeq,
117
- _rpc_background: true,
200
+ let task;
201
+ try {
202
+ task = Promise.resolve(client._callRawV2Rpc('group.v2.ack', {
203
+ group_id: groupId,
204
+ up_to_seq: ackSeq,
205
+ _rpc_background: true,
206
+ }));
207
+ }
208
+ catch (exc) {
209
+ this.finishRealtimeAck(ns, ackSeq);
210
+ client._clientLog.debug(`${label} failed: group=${groupId} seq=${ackSeq} err=${formatDeliveryError(exc)}`);
211
+ return;
212
+ }
213
+ void task.then((result) => {
214
+ if (generation !== null && this.isInlineGenerationCurrent(generation)) {
215
+ this.completePendingGroupInlineAckUpTo(ns, this.resolveActualAckSeq(result, ackSeq), generation);
216
+ }
118
217
  }).catch((e) => {
119
218
  client._clientLog.debug(`${label} failed: group=${groupId} seq=${ackSeq} err=${String(e)}`);
219
+ }).finally(() => {
220
+ if (generation !== null && !this.isInlineGenerationCurrent(generation))
221
+ return;
222
+ const nextAckSeq = this.finishRealtimeAck(ns, ackSeq);
223
+ if (nextAckSeq > 0)
224
+ this.fireGroupV2Ack(groupId, ns, nextAckSeq, label, generation);
225
+ });
226
+ }
227
+ fireP2PV2Ack(seq, label, generation = null) {
228
+ const client = this.runtime.client;
229
+ const ns = client._aid ? `p2p:${client._aid}` : '';
230
+ if (!Number.isSafeInteger(seq) || seq <= 0 || typeof client._ackV2 !== 'function'
231
+ || !this.beginRealtimeAck(ns, seq))
232
+ return;
233
+ try {
234
+ const ackTask = generation === null ? client._ackV2(seq) : client._ackV2(seq, generation);
235
+ void Promise.resolve(ackTask).catch((exc) => {
236
+ client._clientLog.debug(`${label} failed: seq=${seq} err=${formatDeliveryError(exc)}`);
237
+ }).finally(() => {
238
+ if (generation !== null && !this.isInlineGenerationCurrent(generation))
239
+ return;
240
+ const nextAckSeq = this.finishRealtimeAck(ns, seq);
241
+ if (nextAckSeq > 0)
242
+ this.fireP2PV2Ack(nextAckSeq, label, generation);
243
+ });
244
+ }
245
+ catch (exc) {
246
+ this.finishRealtimeAck(ns, seq);
247
+ client._clientLog.debug(`${label} failed: seq=${seq} err=${formatDeliveryError(exc)}`);
248
+ }
249
+ }
250
+ retryPendingP2PInlineAck(ns, seq, label, ackThrough = seq) {
251
+ const pending = this.pendingP2PInlineAcks?.get(ns);
252
+ if (!pending || pending.seq !== seq)
253
+ return false;
254
+ if (pending.acking || !pending.committed)
255
+ return true;
256
+ if (!Number.isSafeInteger(ackThrough) || ackThrough < seq)
257
+ ackThrough = seq;
258
+ const client = this.runtime.client;
259
+ if (typeof client._ackV2 !== 'function')
260
+ return true;
261
+ pending.acking = true;
262
+ const generation = pending.generation;
263
+ let task;
264
+ try {
265
+ task = Promise.resolve(client._ackV2(ackThrough, generation));
266
+ }
267
+ catch (exc) {
268
+ pending.acking = false;
269
+ client._clientLog.debug(`${label} failed: seq=${seq} err=${formatDeliveryError(exc)}`);
270
+ return true;
271
+ }
272
+ void task.catch((exc) => {
273
+ client._clientLog.debug(`${label} failed: seq=${seq} err=${formatDeliveryError(exc)}`);
274
+ }).finally(() => {
275
+ const current = this.pendingP2PInlineAcks?.get(ns);
276
+ if (this.isInlineGenerationCurrent(generation) && current === pending)
277
+ current.acking = false;
120
278
  });
279
+ return true;
280
+ }
281
+ retryPendingGroupInlineAck(ns, seq, label, ackThrough = seq) {
282
+ const pending = this.pendingGroupInlineAcks?.get(ns);
283
+ if (!pending || pending.seq !== seq)
284
+ return false;
285
+ if (pending.acking || !pending.committed)
286
+ return true;
287
+ if (!Number.isSafeInteger(ackThrough) || ackThrough < seq)
288
+ ackThrough = seq;
289
+ const client = this.runtime.client;
290
+ if (typeof client._ackGroupV2 !== 'function')
291
+ return true;
292
+ pending.acking = true;
293
+ const generation = pending.generation;
294
+ let task;
295
+ try {
296
+ task = Promise.resolve(client._ackGroupV2(pending.groupId, ackThrough, pending.groupAid || undefined, generation));
297
+ }
298
+ catch (exc) {
299
+ pending.acking = false;
300
+ client._clientLog.debug(`${label} failed: group=${pending.groupId} seq=${seq} err=${formatDeliveryError(exc)}`);
301
+ return true;
302
+ }
303
+ void task.catch((exc) => {
304
+ client._clientLog.debug(`${label} failed: group=${pending.groupId} seq=${seq} err=${formatDeliveryError(exc)}`);
305
+ }).finally(() => {
306
+ const current = this.pendingGroupInlineAcks?.get(ns);
307
+ if (this.isInlineGenerationCurrent(generation) && current === pending)
308
+ current.acking = false;
309
+ });
310
+ return true;
311
+ }
312
+ reservePendingP2PInlineAck(ns, seq, row, generation = this.inlineGeneration) {
313
+ if (!this.isInlineGenerationCurrent(generation)
314
+ || !ns || !Number.isSafeInteger(seq) || seq <= 0 || this.pendingP2PInlineAcks?.has(ns)
315
+ || this.pendingInlineAckNamespaceCount() >= MAX_INLINE_PENDING_ACK_NAMESPACES)
316
+ return false;
317
+ const pendingMap = this.pendingP2PInlineAcks ?? new Map();
318
+ this.pendingP2PInlineAcks = pendingMap;
319
+ pendingMap.set(ns, {
320
+ seq,
321
+ row: this.inlinePostAckRow(row),
322
+ generation,
323
+ committed: false,
324
+ observedAckSeq: 0,
325
+ acking: false,
326
+ });
327
+ return true;
328
+ }
329
+ dropPendingP2PInlineAck(ns, seq, generation = this.inlineGeneration) {
330
+ const pending = this.pendingP2PInlineAcks?.get(ns);
331
+ if (this.isInlineGenerationCurrent(generation) && pending?.seq === seq && pending.generation === generation) {
332
+ this.pendingP2PInlineAcks?.delete(ns);
333
+ }
334
+ }
335
+ reservePendingGroupInlineAck(ns, seq, row, groupAid, groupId, generation = this.inlineGeneration) {
336
+ if (!this.isInlineGenerationCurrent(generation)
337
+ || !ns || !groupId || !Number.isSafeInteger(seq) || seq <= 0
338
+ || this.pendingGroupInlineAcks?.has(ns)
339
+ || this.pendingGroupInlineAckNamespaces?.has(ns)
340
+ || this.pendingInlineAckNamespaceCount() >= MAX_INLINE_PENDING_ACK_NAMESPACES)
341
+ return false;
342
+ const normalizedGroupAid = typeof groupAid === 'string' ? groupAid.trim() : '';
343
+ const aliasNs = normalizedGroupAid ? `group:${normalizedGroupAid}` : '';
344
+ if (aliasNs && aliasNs !== ns
345
+ && (this.pendingGroupInlineAcks?.has(aliasNs) || this.pendingGroupInlineAckNamespaces?.has(aliasNs)))
346
+ return false;
347
+ const pendingMap = this.pendingGroupInlineAcks ?? new Map();
348
+ this.pendingGroupInlineAcks = pendingMap;
349
+ pendingMap.set(ns, {
350
+ seq,
351
+ row: this.inlinePostAckRow(row),
352
+ generation,
353
+ groupId,
354
+ groupAid: normalizedGroupAid,
355
+ aliasNs: aliasNs !== ns ? aliasNs : '',
356
+ committed: false,
357
+ observedAckSeq: 0,
358
+ acking: false,
359
+ });
360
+ if (aliasNs && aliasNs !== ns) {
361
+ const aliasMap = this.pendingGroupInlineAckNamespaces ?? new Map();
362
+ this.pendingGroupInlineAckNamespaces = aliasMap;
363
+ aliasMap.set(aliasNs, ns);
364
+ }
365
+ return true;
366
+ }
367
+ removePendingGroupInlineAck(ns, pending) {
368
+ if (this.pendingGroupInlineAcks?.get(ns) !== pending)
369
+ return false;
370
+ this.pendingGroupInlineAcks.delete(ns);
371
+ if (pending.aliasNs && this.pendingGroupInlineAckNamespaces?.get(pending.aliasNs) === ns) {
372
+ this.pendingGroupInlineAckNamespaces.delete(pending.aliasNs);
373
+ }
374
+ return true;
375
+ }
376
+ dropPendingGroupInlineAck(ns, seq, generation = this.inlineGeneration) {
377
+ const pending = this.pendingGroupInlineAcks?.get(ns);
378
+ if (this.isInlineGenerationCurrent(generation) && pending?.seq === seq && pending.generation === generation) {
379
+ this.removePendingGroupInlineAck(ns, pending);
380
+ }
381
+ }
382
+ resolvePendingGroupInlineAckNamespace(ns) {
383
+ const pendingMap = this.pendingGroupInlineAcks;
384
+ if (!ns || !pendingMap || pendingMap.size === 0 || pendingMap.has(ns))
385
+ return ns;
386
+ const canonicalNs = this.pendingGroupInlineAckNamespaces?.get(ns);
387
+ return canonicalNs && pendingMap.has(canonicalNs) ? canonicalNs : ns;
388
+ }
389
+ pendingP2PInlineAckGeneration(ns) {
390
+ const pendingMap = this.pendingP2PInlineAcks;
391
+ if (!ns || !pendingMap || pendingMap.size === 0)
392
+ return null;
393
+ const pending = pendingMap.get(ns);
394
+ return pending && this.isInlineGenerationCurrent(pending.generation) ? pending.generation : null;
395
+ }
396
+ pendingGroupInlineAckGeneration(ns) {
397
+ const pendingMap = this.pendingGroupInlineAcks;
398
+ if (!ns || !pendingMap || pendingMap.size === 0)
399
+ return null;
400
+ const canonicalNs = pendingMap.has(ns) ? ns : this.pendingGroupInlineAckNamespaces?.get(ns);
401
+ if (!canonicalNs)
402
+ return null;
403
+ const pending = pendingMap.get(canonicalNs);
404
+ return pending && this.isInlineGenerationCurrent(pending.generation) ? pending.generation : null;
405
+ }
406
+ completePendingP2PInlineAckUpTo(ns, ackSeq, generation = this.inlineGeneration) {
407
+ const pendingMap = this.pendingP2PInlineAcks;
408
+ if (!this.isInlineGenerationCurrent(generation)
409
+ || !ns || !Number.isSafeInteger(ackSeq) || ackSeq <= 0 || !pendingMap || pendingMap.size === 0)
410
+ return;
411
+ const pending = pendingMap.get(ns);
412
+ if (!pending || pending.generation !== generation || ackSeq < pending.seq)
413
+ return;
414
+ if (!pending.committed) {
415
+ pending.observedAckSeq = Math.max(pending.observedAckSeq, ackSeq);
416
+ return;
417
+ }
418
+ pendingMap.delete(ns);
419
+ this.runPostInlineCommit(pending.row, pending.seq, 'P2P', generation);
420
+ }
421
+ completePendingGroupInlineAckUpTo(ns, ackSeq, generation = this.inlineGeneration) {
422
+ const pendingMap = this.pendingGroupInlineAcks;
423
+ if (!this.isInlineGenerationCurrent(generation)
424
+ || !ns || !Number.isSafeInteger(ackSeq) || ackSeq <= 0 || !pendingMap || pendingMap.size === 0)
425
+ return;
426
+ const canonicalNs = this.resolvePendingGroupInlineAckNamespace(ns);
427
+ const pending = pendingMap.get(canonicalNs);
428
+ if (!pending || pending.generation !== generation || ackSeq < pending.seq)
429
+ return;
430
+ if (!pending.committed) {
431
+ pending.observedAckSeq = Math.max(pending.observedAckSeq, ackSeq);
432
+ return;
433
+ }
434
+ this.removePendingGroupInlineAck(canonicalNs, pending);
435
+ this.runPostInlineCommit(pending.row, pending.seq, 'Group', generation);
436
+ }
437
+ runPostInlineCommit(row, seq, scope, generation) {
438
+ const client = this.runtime.client;
439
+ if (!this.isInlineGenerationCurrent(generation) || typeof client._postInlineV2CommitAndAck !== 'function')
440
+ return;
441
+ try {
442
+ void Promise.resolve(client._postInlineV2CommitAndAck(row)).catch((exc) => {
443
+ client._clientLog.debug(`inline ${scope} post-commit work failed: seq=${seq} err=${formatDeliveryError(exc)}`);
444
+ });
445
+ }
446
+ catch (exc) {
447
+ client._clientLog.debug(`inline ${scope} post-commit work failed: seq=${seq} err=${formatDeliveryError(exc)}`);
448
+ }
449
+ }
450
+ markPendingP2PInlineAckCommitted(ns, seq, generation = this.inlineGeneration) {
451
+ const pending = this.pendingP2PInlineAcks?.get(ns);
452
+ if (!this.isInlineGenerationCurrent(generation) || !pending || pending.seq !== seq || pending.generation !== generation)
453
+ return false;
454
+ pending.committed = true;
455
+ if (pending.observedAckSeq < seq)
456
+ return true;
457
+ this.pendingP2PInlineAcks?.delete(ns);
458
+ this.runPostInlineCommit(pending.row, pending.seq, 'P2P', generation);
459
+ return true;
460
+ }
461
+ markPendingGroupInlineAckCommitted(ns, seq, generation = this.inlineGeneration) {
462
+ const pending = this.pendingGroupInlineAcks?.get(ns);
463
+ if (!this.isInlineGenerationCurrent(generation) || !pending || pending.seq !== seq || pending.generation !== generation)
464
+ return false;
465
+ pending.committed = true;
466
+ if (pending.observedAckSeq < seq)
467
+ return true;
468
+ this.removePendingGroupInlineAck(ns, pending);
469
+ this.runPostInlineCommit(pending.row, pending.seq, 'Group', generation);
470
+ return true;
471
+ }
472
+ resolveActualAckSeq(result, requestedSeq) {
473
+ if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
474
+ return 0;
475
+ if (!isJsonObject(result))
476
+ return requestedSeq;
477
+ const row = result;
478
+ let value;
479
+ if (Object.prototype.hasOwnProperty.call(row, 'effective_ack_seq')) {
480
+ value = row.effective_ack_seq;
481
+ }
482
+ else if (Object.prototype.hasOwnProperty.call(row, 'ack_seq')) {
483
+ value = row.ack_seq;
484
+ }
485
+ else if (Object.prototype.hasOwnProperty.call(row, 'cursor')) {
486
+ const cursor = row.cursor;
487
+ if (isJsonObject(cursor)) {
488
+ const cursorRow = cursor;
489
+ if (!Object.prototype.hasOwnProperty.call(cursorRow, 'current_seq'))
490
+ return 0;
491
+ value = cursorRow.current_seq;
492
+ }
493
+ else {
494
+ value = cursor;
495
+ }
496
+ }
497
+ else {
498
+ return requestedSeq;
499
+ }
500
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
121
501
  }
122
502
  enqueueOrderedMessage(ns, event, seq, payload) {
123
503
  const client = this.runtime.client;
@@ -136,7 +516,7 @@ export class MessageDeliveryEngine {
136
516
  isGroupEventNamespace(ns) {
137
517
  return ns.startsWith('group_event:');
138
518
  }
139
- async publishOrderedQueueItem(ns, event, seq, payload) {
519
+ async publishOrderedQueueItem(ns, event, seq, payload, pullResponse = false) {
140
520
  const client = this.runtime.client;
141
521
  if (event === 'group.changed' && this.isGroupEventNamespace(ns)) {
142
522
  await this.publishOrderedGroupChanged(payload);
@@ -146,6 +526,10 @@ export class MessageDeliveryEngine {
146
526
  await this.publishMessageRecallTombstone(seq, payload);
147
527
  return;
148
528
  }
529
+ if (pullResponse) {
530
+ await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload));
531
+ return;
532
+ }
149
533
  await client._publishAppEvent(event, payload);
150
534
  }
151
535
  async publishOrderedGroupChanged(payload) {
@@ -154,8 +538,8 @@ export class MessageDeliveryEngine {
154
538
  const eventPayload = payload;
155
539
  client._groupState?.handleGroupChangedV2Membership?.(eventPayload);
156
540
  if (eventPayload.action === 'dissolved') {
157
- const rawGroupId = eventPayload.group_aid ?? eventPayload.group_id ?? '';
158
- const groupId = normalizeGroupId(String(rawGroupId)) || String(rawGroupId).trim();
541
+ const rawGroupId = eventPayload.group_id ?? eventPayload.group_aid ?? '';
542
+ const groupId = String(rawGroupId).trim();
159
543
  if (groupId)
160
544
  client._cleanupDissolvedGroup?.(groupId);
161
545
  }
@@ -185,13 +569,28 @@ export class MessageDeliveryEngine {
185
569
  }
186
570
  normalizePublishedMessagePayload(event, payload) {
187
571
  if (this.isInstanceScopedMessageEvent(event)) {
572
+ payload = this.stripInlineInternalFields(payload);
573
+ if (event === 'group.message_created')
574
+ payload = normalizeGroupMentionMode(payload);
188
575
  return this.attachAppMessageEnvelope(this.stripInternalSenderDeviceFields(this.attachCurrentInstanceContext(payload)));
189
576
  }
190
577
  if (this.isGroupScopedEvent(event)) {
578
+ payload = this.stripInlineInternalFields(payload);
191
579
  return this.attachAppGroupEventEnvelope(this.attachCurrentInstanceContext(payload));
192
580
  }
193
581
  return payload;
194
582
  }
583
+ stripInlineInternalFields(payload) {
584
+ if (!isJsonObject(payload))
585
+ return payload;
586
+ if (!Object.prototype.hasOwnProperty.call(payload, 'inline_message')
587
+ && !Object.prototype.hasOwnProperty.call(payload, '_inline_message_candidates_v1'))
588
+ return payload;
589
+ const result = { ...payload };
590
+ delete result.inline_message;
591
+ delete result._inline_message_candidates_v1;
592
+ return result;
593
+ }
195
594
  envelopeMetadata(value) {
196
595
  let source = value;
197
596
  if (source && typeof source === 'object') {
@@ -248,6 +647,7 @@ export class MessageDeliveryEngine {
248
647
  if (protectedHeaders)
249
648
  envelope.protected_headers = protectedHeaders;
250
649
  setIfPresent('payload_type', firstValue(message.payload_type, protectedHeaders?.payload_type));
650
+ setIfPresent('mention_mode', message.mention_mode);
251
651
  return envelope;
252
652
  }
253
653
  isGroupScopedEvent(event) {
@@ -314,19 +714,30 @@ export class MessageDeliveryEngine {
314
714
  if (protectedHeaders)
315
715
  envelope.protected_headers = protectedHeaders;
316
716
  setIfPresent('payload_type', firstValue(params.payload_type, protectedHeaders?.payload_type, body.type));
717
+ if (method === 'group.send')
718
+ setIfPresent('mention_mode', resultObj.mention_mode);
317
719
  return envelope;
318
720
  }
319
- attachSendResultEnvelope(method, params, result, encrypted) {
721
+ attachSendResultEnvelope(method, params, result, encrypted, modeSource) {
320
722
  if (!APP_SEND_ENVELOPE_METHODS.has(method) || !isJsonObject(result))
321
723
  return result;
322
- const out = { ...result };
323
- out.envelope = this.sendResultEnvelope(method, params, out, encrypted);
724
+ let out = { ...result };
725
+ const serverEnvelope = method === 'group.send' && isJsonObject(out.envelope)
726
+ ? { ...out.envelope }
727
+ : undefined;
324
728
  if ('payload' in params) {
325
729
  out.payload = params.payload;
326
730
  }
327
731
  else if ('content' in params) {
328
732
  out.payload = params.content;
329
733
  }
734
+ const generatedEnvelope = this.sendResultEnvelope(method, params, out, encrypted);
735
+ out.envelope = serverEnvelope ? { ...generatedEnvelope, ...serverEnvelope } : generatedEnvelope;
736
+ if (method === 'group.send') {
737
+ const serverSource = modeSource ?? result;
738
+ const authoritativeSource = hasGroupMentionModeField(serverSource) ? serverSource : out;
739
+ out = normalizeGroupMentionMode(out, authoritativeSource);
740
+ }
330
741
  return out;
331
742
  }
332
743
  attachAppGroupEventEnvelope(payload) {
@@ -516,7 +927,7 @@ export class MessageDeliveryEngine {
516
927
  // (group_id, sorted message_ids) 已能唯一标识一次撤回;缺 message_ids 时按原消息 id/seq 兜底。
517
928
  // 去重键不含 recalled_at:占位/通知 tombstone(pull)与在线 push 三条通道对同一次撤回
518
929
  // 可能携带不同来源的时间戳(push 在事务后重取),纳入 recalled_at 会使去重失效、回调多次。
519
- const normalizedGroupId = normalizeGroupId(groupId) || String(groupId ?? '').trim();
930
+ const normalizedGroupId = String(groupId ?? '').trim();
520
931
  const ids = payload.message_ids;
521
932
  const idPart = Array.isArray(ids)
522
933
  ? ids.map((i) => String(i ?? '').trim()).filter(Boolean).sort().join(',')
@@ -545,9 +956,7 @@ export class MessageDeliveryEngine {
545
956
  if (!eventPayload)
546
957
  return false;
547
958
  const rawGroupId = String(eventPayload.group_id ?? groupId ?? '').trim();
548
- const dedupGroupId = normalizeGroupId(rawGroupId) || rawGroupId;
549
- if (dedupGroupId)
550
- eventPayload.group_id = dedupGroupId;
959
+ const dedupGroupId = rawGroupId;
551
960
  const dedupKey = this.groupRecallDedupKey(dedupGroupId, eventPayload);
552
961
  let seen = client._groupRecallSeen;
553
962
  if (!seen) {
@@ -645,7 +1054,7 @@ export class MessageDeliveryEngine {
645
1054
  }
646
1055
  if (isJsonObject(payload)) {
647
1056
  try {
648
- const snapshot = client._agentMdManager.eventSnapshot();
1057
+ const snapshot = await client._agentMdManager.eventSnapshot();
649
1058
  const localEtag = snapshot?.local_etag || '';
650
1059
  const remoteEtag = snapshot?.remote_etag || '';
651
1060
  if ((localEtag || remoteEtag) && payload._agent_md === undefined) {
@@ -665,24 +1074,76 @@ export class MessageDeliveryEngine {
665
1074
  if (!isJsonObject(message))
666
1075
  return true;
667
1076
  const client = this.runtime.client;
668
- if ('device_id' in message) {
669
- const targetDeviceId = String(message.device_id ?? '').trim();
670
- if (targetDeviceId !== client._deviceId) {
1077
+ const explicitDevice = this.strictTargetString(message, 'target_device_id');
1078
+ if (explicitDevice.present) {
1079
+ if (!explicitDevice.valid || explicitDevice.value !== client._deviceId)
1080
+ return false;
1081
+ }
1082
+ else {
1083
+ const targetDeviceId = this.boundString(message, ['device_id']);
1084
+ if (targetDeviceId.present && targetDeviceId.value !== client._deviceId) {
671
1085
  return false;
672
1086
  }
673
1087
  }
674
- if ('slot_id' in message) {
675
- const targetSlotId = String(message.slot_id ?? '').trim();
676
- if (slotIsolationKey(targetSlotId) !== slotIsolationKey(client._slotId)) {
1088
+ const explicitSlot = this.strictTargetString(message, 'target_slot_id');
1089
+ if (explicitSlot.present) {
1090
+ if (!explicitSlot.valid
1091
+ || slotIsolationKey(explicitSlot.value) !== slotIsolationKey(client._slotId))
1092
+ return false;
1093
+ }
1094
+ else {
1095
+ const targetSlotId = this.boundString(message, ['slot_id']);
1096
+ if (targetSlotId.present
1097
+ && slotIsolationKey(targetSlotId.value) !== slotIsolationKey(client._slotId)) {
677
1098
  return false;
678
1099
  }
679
1100
  }
680
1101
  return true;
681
1102
  }
682
- async runPushProcessSerialized(ns, operation) {
1103
+ strictTargetString(source, key) {
1104
+ if (!Object.prototype.hasOwnProperty.call(source, key)) {
1105
+ return { present: false, valid: false, value: '' };
1106
+ }
1107
+ const raw = source[key];
1108
+ if (typeof raw !== 'string')
1109
+ return { present: true, valid: false, value: '' };
1110
+ const value = raw.trim();
1111
+ return { present: true, valid: Boolean(value), value };
1112
+ }
1113
+ async runPushProcessSerialized(ns, operation, inlineGeneration) {
683
1114
  const client = this.runtime.client;
684
- if (!ns) {
1115
+ if (inlineGeneration === undefined) {
1116
+ if (!ns) {
1117
+ await operation();
1118
+ return;
1119
+ }
1120
+ let queues = client._pushProcessQueues;
1121
+ if (!queues) {
1122
+ queues = new Map();
1123
+ client._pushProcessQueues = queues;
1124
+ }
1125
+ const previous = queues.get(ns) ?? Promise.resolve();
1126
+ const current = previous.catch(() => undefined).then(operation);
1127
+ const stored = current.then(() => undefined, () => undefined);
1128
+ queues.set(ns, stored);
1129
+ try {
1130
+ await current;
1131
+ }
1132
+ finally {
1133
+ if (queues.get(ns) === stored)
1134
+ queues.delete(ns);
1135
+ }
1136
+ return;
1137
+ }
1138
+ const run = async () => {
1139
+ if (!this.isInlineGenerationCurrent(inlineGeneration))
1140
+ return;
685
1141
  await operation();
1142
+ if (!this.isInlineGenerationCurrent(inlineGeneration))
1143
+ return;
1144
+ };
1145
+ if (!ns) {
1146
+ await run();
686
1147
  return;
687
1148
  }
688
1149
  let queues = client._pushProcessQueues;
@@ -691,7 +1152,7 @@ export class MessageDeliveryEngine {
691
1152
  client._pushProcessQueues = queues;
692
1153
  }
693
1154
  const previous = queues.get(ns) ?? Promise.resolve();
694
- const current = previous.catch(() => undefined).then(operation);
1155
+ const current = previous.catch(() => undefined).then(run);
695
1156
  const stored = current.then(() => undefined, () => undefined);
696
1157
  queues.set(ns, stored);
697
1158
  try {
@@ -708,7 +1169,18 @@ export class MessageDeliveryEngine {
708
1169
  const ns = isJsonObject(data) && client._aid && data.seq !== undefined
709
1170
  ? `p2p:${client._aid}`
710
1171
  : '';
711
- client._safeAsync(this.runPushProcessSerialized(ns, () => this.processAndPublishMessage(data)));
1172
+ const seq = isJsonObject(data) && typeof data.seq === 'number' ? data.seq : 0;
1173
+ if (client._v2Session && ns && Number.isSafeInteger(seq) && seq > 0
1174
+ && this.messageTargetsCurrentInstance(data)) {
1175
+ client._seqTracker.updateMaxSeen(ns, seq);
1176
+ }
1177
+ const hasInlineObject = isJsonObject(data)
1178
+ && isJsonObject(data.inline_message);
1179
+ const inlineGeneration = hasInlineObject ? this.captureInlineGeneration() : undefined;
1180
+ const operation = () => this.processAndPublishMessage(data, inlineGeneration);
1181
+ client._safeAsync(client._v2Session && ns && !hasInlineObject
1182
+ ? operation()
1183
+ : this.runPushProcessSerialized(ns, operation, inlineGeneration));
712
1184
  client._clientLog.debug('_onRawMessageReceived exit: elapsed=0ms (dispatched async)');
713
1185
  }
714
1186
  async onRawMessageRecalled(data) {
@@ -729,7 +1201,7 @@ export class MessageDeliveryEngine {
729
1201
  const seqNeedsPull = client._seqTracker.onMessageSeq(ns, seq);
730
1202
  const published = await this.publishOrderedMessage('message.recalled', ns, seq, msg);
731
1203
  const contigAfter = client._seqTracker.getContiguousSeq(ns);
732
- if (seqNeedsPull && !published) {
1204
+ if (seqNeedsPull && !published && client._sessionOptions?.background_sync !== false) {
733
1205
  client._safeAsync(this.fillP2pGap());
734
1206
  }
735
1207
  const contig = client._seqTracker.getContiguousSeq(ns);
@@ -748,7 +1220,7 @@ export class MessageDeliveryEngine {
748
1220
  }
749
1221
  await this.publishMessageRecallTombstone(seq, msg);
750
1222
  }
751
- async processAndPublishMessage(data) {
1223
+ async processAndPublishMessage(data, inlineGeneration) {
752
1224
  const client = this.runtime.client;
753
1225
  try {
754
1226
  if (!isJsonObject(data)) {
@@ -759,6 +1231,15 @@ export class MessageDeliveryEngine {
759
1231
  if (!this.messageTargetsCurrentInstance(msg)) {
760
1232
  return;
761
1233
  }
1234
+ // 新 SDK 的所有带合法 seq P2P Push 都只作为 Head 通知处理。
1235
+ // 新服务端为兼容旧 SDK 仍可能发送 message.received;V2 会话下必须
1236
+ // 在旧 seq tracking / ordered publish 之前转入前台 Tail 协调器。
1237
+ const legacyPushSeq = positiveSafeSequenceHint(msg.seq);
1238
+ const hasInlineField = Object.prototype.hasOwnProperty.call(msg, 'inline_message');
1239
+ if (client._v2Session && client._aid && (legacyPushSeq > 0 || hasInlineField)) {
1240
+ await this.processV2P2PNotification(msg, 'v1', inlineGeneration);
1241
+ return;
1242
+ }
762
1243
  const seq = msg.seq;
763
1244
  const encryptedPush = client._isEncryptedPushMessage(msg);
764
1245
  if (seq !== undefined && seq !== null && client._aid) {
@@ -772,7 +1253,7 @@ export class MessageDeliveryEngine {
772
1253
  : await this.publishOrderedMessage('message.received', ns, seq, msg);
773
1254
  const contigAfter = client._seqTracker.getContiguousSeq(ns);
774
1255
  const needPull = seqNeedsPull && !published;
775
- if (needPull) {
1256
+ if (needPull && client._sessionOptions?.background_sync !== false) {
776
1257
  client._safeAsync(this.fillP2pGap());
777
1258
  }
778
1259
  const contig = client._seqTracker.getContiguousSeq(ns);
@@ -818,11 +1299,24 @@ export class MessageDeliveryEngine {
818
1299
  onRawGroupMessageCreated(data) {
819
1300
  const client = this.runtime.client;
820
1301
  client._clientLog.debug(`_onRawGroupMessageCreated enter: group_id=${data?.group_id ?? '-'} from=${data?.from ?? '-'} seq=${data?.seq ?? '-'}`);
821
- const groupId = isJsonObject(data) ? String(data.group_id ?? '').trim() : '';
822
- client._safeAsync(this.runPushProcessSerialized(groupId ? `group:${groupId}` : '', () => this.processAndPublishGroupMessage(data)));
1302
+ const groupId = isJsonObject(data) && typeof data.group_id === 'string' ? data.group_id.trim() : '';
1303
+ const seq = isJsonObject(data) && typeof data.seq === 'number' ? data.seq : 0;
1304
+ const kind = isJsonObject(data) ? String(data.kind ?? '').trim() : '';
1305
+ if (client._v2Session && groupId && kind !== 'group.online_unread_hint'
1306
+ && Number.isSafeInteger(seq) && seq > 0) {
1307
+ client._seqTracker.updateMaxSeen(`group:${groupId}`, seq);
1308
+ }
1309
+ const ns = groupId ? `group:${groupId}` : '';
1310
+ const hasInlineObject = isJsonObject(data)
1311
+ && isJsonObject(data.inline_message);
1312
+ const inlineGeneration = hasInlineObject ? this.captureInlineGeneration() : undefined;
1313
+ const operation = () => this.processAndPublishGroupMessage(data, inlineGeneration);
1314
+ client._safeAsync(client._v2Session && ns && !hasInlineObject
1315
+ ? operation()
1316
+ : this.runPushProcessSerialized(ns, operation, inlineGeneration));
823
1317
  client._clientLog.debug('_onRawGroupMessageCreated exit: elapsed=0ms (dispatched async)');
824
1318
  }
825
- async processAndPublishGroupMessage(data) {
1319
+ async processAndPublishGroupMessage(data, inlineGeneration) {
826
1320
  const client = this.runtime.client;
827
1321
  try {
828
1322
  if (!isJsonObject(data)) {
@@ -830,12 +1324,20 @@ export class MessageDeliveryEngine {
830
1324
  return;
831
1325
  }
832
1326
  const msg = { ...data };
833
- const groupId = (msg.group_id ?? '');
1327
+ const groupId = typeof msg.group_id === 'string' ? msg.group_id.trim() : '';
834
1328
  const seq = msg.seq;
835
1329
  const payload = msg.payload;
836
1330
  if (groupId) {
837
1331
  client._groupSynced.add(groupId);
838
1332
  }
1333
+ // V2 会话中旧 group.message_created 也只是 Head 通知;必须先 Tail,
1334
+ // 不能让 payload 路径直接推进旧 ordered publish 状态。
1335
+ const legacyPushSeq = positiveSafeSequenceHint(seq);
1336
+ const hasInlineField = Object.prototype.hasOwnProperty.call(msg, 'inline_message');
1337
+ if (client._v2Session && groupId && (legacyPushSeq > 0 || hasInlineField)) {
1338
+ await this.processGroupV2MessageCreated(msg, 'v1', inlineGeneration);
1339
+ return;
1340
+ }
839
1341
  if (payload === undefined || payload === null
840
1342
  || (typeof payload === 'object' && Object.keys(payload).length === 0)) {
841
1343
  await this.autoPullGroupMessages(msg);
@@ -873,7 +1375,7 @@ export class MessageDeliveryEngine {
873
1375
  : await this.publishOrderedMessage('group.message_created', ns, seq, msg);
874
1376
  const contigAfter = client._seqTracker.getContiguousSeq(ns);
875
1377
  const needPull = seqNeedsPull && !published;
876
- if (needPull) {
1378
+ if (needPull && client._sessionOptions?.background_sync !== false) {
877
1379
  client._safeAsync(this.fillGroupGap(groupId));
878
1380
  }
879
1381
  const contig = client._seqTracker.getContiguousSeq(ns);
@@ -924,11 +1426,16 @@ export class MessageDeliveryEngine {
924
1426
  await client._publishAppEvent('group.message_created', notification);
925
1427
  return;
926
1428
  }
927
- const ns = `group:${groupId}`;
1429
+ if (client._sessionOptions?.background_sync === false) {
1430
+ client._clientLog.debug(`auto pull group messages skipped: group=${groupId} background_sync=false`);
1431
+ return;
1432
+ }
1433
+ const ns = this.resolvePendingGroupInlineAckNamespace(`group:${groupId}`);
928
1434
  const afterSeq = client._seqTracker.getContiguousSeq(ns);
929
1435
  try {
930
1436
  if (client._v2Session) {
931
- await client._withBackgroundRpc(() => client._pullGroupV2Internal({ group_id: groupId, after_seq: afterSeq, limit: 50 }));
1437
+ const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 });
1438
+ await client._rpcPipeline.runPullSerialized(key, () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true }), true);
932
1439
  return;
933
1440
  }
934
1441
  const result = await client.call('group.pull', {
@@ -994,6 +1501,8 @@ export class MessageDeliveryEngine {
994
1501
  }
995
1502
  async fillP2pGap(retryAttempt = 0) {
996
1503
  const client = this.runtime.client;
1504
+ if (client._sessionOptions?.background_sync === false)
1505
+ return;
997
1506
  if (client.state !== ConnectionState.READY || client._closing)
998
1507
  return;
999
1508
  if (!client._aid)
@@ -1007,7 +1516,8 @@ export class MessageDeliveryEngine {
1007
1516
  this.runtime.delivery.setGapFillActive(true);
1008
1517
  let filled = 0;
1009
1518
  try {
1010
- const messages = await client._withBackgroundRpc(() => client._pullV2(afterSeq, 50));
1519
+ const key = client._rpcPipeline.pullGateKeyForCall('message.pull', { after_seq: afterSeq, limit: 50 });
1520
+ const messages = await client._rpcPipeline.runPullSerialized(key, () => client._pullV2(afterSeq, 50, { gateLocked: true }), true);
1011
1521
  filled = messages.length;
1012
1522
  this.prunePushedSeqs(ns);
1013
1523
  }
@@ -1064,9 +1574,11 @@ export class MessageDeliveryEngine {
1064
1574
  }
1065
1575
  async fillGroupGap(groupId) {
1066
1576
  const client = this.runtime.client;
1577
+ if (client._sessionOptions?.background_sync === false)
1578
+ return;
1067
1579
  if (client.state !== ConnectionState.READY || client._closing)
1068
1580
  return;
1069
- groupId = normalizeGroupId(groupId) || String(groupId ?? '').trim();
1581
+ groupId = String(groupId ?? '').trim();
1070
1582
  if (!groupId)
1071
1583
  return;
1072
1584
  const ns = `group:${groupId}`;
@@ -1078,7 +1590,8 @@ export class MessageDeliveryEngine {
1078
1590
  this.runtime.delivery.setGapFillActive(true);
1079
1591
  let filled = 0;
1080
1592
  try {
1081
- const messages = await client._withBackgroundRpc(() => client._pullGroupV2(groupId, afterSeq, 50));
1593
+ const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 });
1594
+ const messages = await client._rpcPipeline.runPullSerialized(key, () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true }), true);
1082
1595
  filled = messages.length;
1083
1596
  this.prunePushedSeqs(ns);
1084
1597
  }
@@ -1095,9 +1608,11 @@ export class MessageDeliveryEngine {
1095
1608
  }
1096
1609
  async fillGroupEventGap(groupId) {
1097
1610
  const client = this.runtime.client;
1611
+ if (client._sessionOptions?.background_sync === false)
1612
+ return;
1098
1613
  if (client.state !== ConnectionState.READY || client._closing)
1099
1614
  return;
1100
- groupId = normalizeGroupId(groupId) || String(groupId ?? '').trim();
1615
+ groupId = String(groupId ?? '').trim();
1101
1616
  if (!groupId)
1102
1617
  return;
1103
1618
  const ns = `group_event:${groupId}`;
@@ -1131,12 +1646,16 @@ export class MessageDeliveryEngine {
1131
1646
  client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq);
1132
1647
  }
1133
1648
  const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1134
- const serverAck = cursor ? Number(cursor.current_seq ?? 0) : 0;
1135
- if (serverAck > 0) {
1649
+ const retentionFloor = Math.max(0, Number(result.retention_floor_event_seq ?? 0), Number(cursor?.retention_floor_seq ?? 0));
1650
+ const serverAckFloor = Math.max(0, Number(cursor?.current_seq ?? 0));
1651
+ const earliestAvailable = Number(result.earliest_available_event_seq ?? 0);
1652
+ const visibilityFloor = Math.max(0, Number(cursor?.join_seq ?? 0), Number.isFinite(earliestAvailable) && earliestAvailable > 0 ? earliestAvailable - 1 : 0);
1653
+ const effectiveFloor = Math.max(...[retentionFloor, serverAckFloor, visibilityFloor].filter((value) => Number.isFinite(value)));
1654
+ if (effectiveFloor > 0) {
1136
1655
  const contigBeforeFloor = client._seqTracker.getContiguousSeq(ns);
1137
- if (contigBeforeFloor < serverAck) {
1138
- client._clientLog.info('group.pull_events retention-floor advance: ns=' + ns + ' contiguous=' + contigBeforeFloor + ' -> cursor.current_seq=' + serverAck);
1139
- client._seqTracker.forceContiguousSeq(ns, serverAck);
1656
+ if (contigBeforeFloor < effectiveFloor) {
1657
+ client._clientLog.info('group.pull_events floor advance: ns=' + ns + ' contiguous=' + contigBeforeFloor + ' -> effective_floor=' + effectiveFloor + ' retention_floor=' + retentionFloor + ' server_ack=' + serverAckFloor + ' visibility_floor=' + visibilityFloor);
1658
+ client._seqTracker.forceContiguousSeq(ns, effectiveFloor);
1140
1659
  }
1141
1660
  }
1142
1661
  const eventSeqs = [];
@@ -1171,10 +1690,9 @@ export class MessageDeliveryEngine {
1171
1690
  this.persistSeq(ns);
1172
1691
  }
1173
1692
  if (eventObjects.length > 0 && ackContig > 0 && ackContig !== pageContigBefore) {
1174
- const ackSeq = this.clampAckSeq('group.ack_events', 'event_seq', ns, ackContig);
1175
1693
  client._transport.call('group.ack_events', {
1176
1694
  group_id: groupId,
1177
- event_seq: ackSeq,
1695
+ event_seq: ackContig,
1178
1696
  device_id: client._deviceId,
1179
1697
  slot_id: client._slotId,
1180
1698
  _rpc_background: true,
@@ -1199,7 +1717,7 @@ export class MessageDeliveryEngine {
1199
1717
  }
1200
1718
  async handleGroupChangedEventSeq(data, groupId) {
1201
1719
  const client = this.runtime.client;
1202
- groupId = normalizeGroupId(groupId) || String(groupId ?? '').trim();
1720
+ groupId = String(groupId ?? '').trim();
1203
1721
  let needPull = false;
1204
1722
  const rawEventSeq = data.event_seq;
1205
1723
  const eventSeq = Number(rawEventSeq);
@@ -1208,6 +1726,15 @@ export class MessageDeliveryEngine {
1208
1726
  return;
1209
1727
  }
1210
1728
  const ns = `group_event:${groupId}`;
1729
+ if (String(data.kind ?? '').trim() === 'group.online_unread_hint') {
1730
+ if (client._sessionOptions?.background_sync === false) {
1731
+ client._clientLog.debug(`group.changed online unread hint skipped: group=${groupId} background_sync=false`);
1732
+ return;
1733
+ }
1734
+ client._clientLog.debug(`group.changed online unread hint triggering gap fill: group=${groupId}, event_seq=${eventSeq}`);
1735
+ client._safeAsync(this.fillGroupEventGap(groupId));
1736
+ return;
1737
+ }
1211
1738
  if (this.isSelfJoinGroupChanged(data)) {
1212
1739
  const contig = client._seqTracker.getContiguousSeq(ns);
1213
1740
  const maxSeen = client._seqTracker.getMaxSeenSeq(ns);
@@ -1235,10 +1762,9 @@ export class MessageDeliveryEngine {
1235
1762
  if (ackContig > 0 && ackContig !== contigBefore) {
1236
1763
  if (data.action !== 'dissolved')
1237
1764
  this.persistSeq(ns);
1238
- const ackSeq = this.clampAckSeq('group.ack_events', 'event_seq', ns, ackContig);
1239
1765
  client._transport.call('group.ack_events', {
1240
1766
  group_id: groupId,
1241
- event_seq: ackSeq,
1767
+ event_seq: ackContig,
1242
1768
  device_id: client._deviceId,
1243
1769
  slot_id: client._slotId,
1244
1770
  _rpc_background: true,
@@ -1246,7 +1772,7 @@ export class MessageDeliveryEngine {
1246
1772
  client._clientLog.warn('group event push auto-ack failed: group=' + groupId, e);
1247
1773
  });
1248
1774
  }
1249
- if (needPull && groupId && !data._from_gap_fill) {
1775
+ if (needPull && groupId && !data._from_gap_fill && client._sessionOptions?.background_sync !== false) {
1250
1776
  client._safeAsync(this.fillGroupEventGap(groupId));
1251
1777
  }
1252
1778
  }
@@ -1282,12 +1808,11 @@ export class MessageDeliveryEngine {
1282
1808
  if (client._onlineUnreadHintDrainActive)
1283
1809
  return;
1284
1810
  this.runtime.delivery.setOnlineUnreadHintDrainActive(true);
1811
+ const attemptedAckByGroup = new Map();
1285
1812
  try {
1286
1813
  while (client._onlineUnreadHintQueue.size > 0) {
1287
1814
  if (client.state !== ConnectionState.READY)
1288
1815
  return;
1289
- if (client._sessionOptions?.background_sync === false)
1290
- return;
1291
1816
  const groupId = client._onlineUnreadHintQueue.keys().next().value;
1292
1817
  if (!groupId)
1293
1818
  return;
@@ -1295,6 +1820,11 @@ export class MessageDeliveryEngine {
1295
1820
  client._onlineUnreadHintQueue.delete(groupId);
1296
1821
  if (!payload)
1297
1822
  continue;
1823
+ const currentAck = this.syncState(`group:${groupId}`).ack;
1824
+ const attemptedAck = attemptedAckByGroup.get(groupId);
1825
+ if (attemptedAck !== undefined && currentAck <= attemptedAck)
1826
+ continue;
1827
+ attemptedAckByGroup.set(groupId, currentAck);
1298
1828
  await this.onRawGroupV2MessageCreated({ ...payload, _online_hint_drained: true });
1299
1829
  const intervalMs = Math.max(0, Number(client._onlineUnreadHintIntervalMs ?? 50) || 0);
1300
1830
  if (intervalMs > 0 && client._onlineUnreadHintQueue.size > 0) {
@@ -1309,142 +1839,795 @@ export class MessageDeliveryEngine {
1309
1839
  this.runtime.delivery.setOnlineUnreadHintDrainActive(false);
1310
1840
  }
1311
1841
  }
1312
- async onRawGroupV2MessageCreated(data) {
1842
+ syncState(ns) {
1843
+ const tracker = this.runtime.client._seqTracker;
1844
+ if (typeof tracker.getSyncState === 'function')
1845
+ return tracker.getSyncState(ns);
1846
+ const ack = Number(tracker.getContiguousSeq(ns) || 0);
1847
+ return { ack, tail: ack, head: Math.max(ack, Number(tracker.getMaxSeenSeq?.(ns) || 0)) };
1848
+ }
1849
+ inlineMessage(data) {
1850
+ if (!isJsonObject(data))
1851
+ return null;
1852
+ const inline = data.inline_message;
1853
+ return isJsonObject(inline)
1854
+ ? inline
1855
+ : null;
1856
+ }
1857
+ boundString(source, keys) {
1858
+ for (const key of keys) {
1859
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
1860
+ return { present: true, value: String(source[key] ?? '').trim() };
1861
+ }
1862
+ }
1863
+ return { present: false, value: '' };
1864
+ }
1865
+ sameRequiredBinding(left, leftKeys, right, rightKeys) {
1866
+ const lhs = this.consistentBindingValue(left, leftKeys);
1867
+ const rhs = this.consistentBindingValue(right, rightKeys);
1868
+ return lhs.present && rhs.present && Boolean(lhs.value) && lhs.value === rhs.value;
1869
+ }
1870
+ consistentBindingValue(source, keys) {
1871
+ const binding = this.strictBoundString(source, keys);
1872
+ return binding.present && binding.valid
1873
+ ? { present: true, value: binding.value }
1874
+ : { present: false, value: '' };
1875
+ }
1876
+ strictBoundString(source, keys) {
1877
+ let value = null;
1878
+ for (const key of keys) {
1879
+ if (!Object.prototype.hasOwnProperty.call(source, key))
1880
+ continue;
1881
+ const raw = source[key];
1882
+ if (typeof raw !== 'string')
1883
+ return { present: true, valid: false, value: '' };
1884
+ const candidate = raw.trim();
1885
+ if (!candidate || (value !== null && candidate !== value)) {
1886
+ return { present: true, valid: false, value: '' };
1887
+ }
1888
+ value = candidate;
1889
+ }
1890
+ return { present: value !== null, valid: value !== null, value: value ?? '' };
1891
+ }
1892
+ onlyEmptyOptionalStrings(source, keys) {
1893
+ for (const key of keys) {
1894
+ if (!Object.prototype.hasOwnProperty.call(source, key))
1895
+ continue;
1896
+ const raw = source[key];
1897
+ if (typeof raw !== 'string' || raw.trim())
1898
+ return false;
1899
+ }
1900
+ return true;
1901
+ }
1902
+ inlineTargetMatchesCurrentInstance(head, inline) {
1313
1903
  const client = this.runtime.client;
1314
- const tStart = Date.now();
1315
- if (!isJsonObject(data)) {
1316
- client._clientLog.debug(`_onRawGroupV2MessageCreated skipped: non-object type=${typeof data}`);
1317
- return;
1904
+ const explicitHeadDevice = this.strictTargetString(head, 'target_device_id');
1905
+ if (explicitHeadDevice.present && !explicitHeadDevice.valid)
1906
+ return false;
1907
+ const headDevice = explicitHeadDevice.present
1908
+ ? explicitHeadDevice
1909
+ : this.strictBoundString(head, ['device_id']);
1910
+ if (!headDevice.present || !headDevice.valid
1911
+ || headDevice.value !== String(client._deviceId ?? '').trim())
1912
+ return false;
1913
+ const explicitRowDevice = this.strictTargetString(inline, 'target_device_id');
1914
+ if (explicitRowDevice.present
1915
+ && (!explicitRowDevice.valid || explicitRowDevice.value !== headDevice.value))
1916
+ return false;
1917
+ if (!explicitRowDevice.present) {
1918
+ const rowDevice = this.strictBoundString(inline, ['device_id']);
1919
+ if (rowDevice.present && (!rowDevice.valid || rowDevice.value !== headDevice.value))
1920
+ return false;
1318
1921
  }
1319
- const groupId = String(data.group_id ?? '').trim();
1320
- const seq = Number(data.seq ?? 0) || 0;
1321
- const messageId = String(data.message_id ?? '').trim();
1322
- const senderAid = String(data.sender_aid ?? '').trim();
1323
- client._clientLog.debug(`_onRawGroupV2MessageCreated group=${groupId} seq=${seq} message_id=${messageId} sender=${senderAid}`);
1324
- if (!groupId || seq <= 0) {
1325
- client._clientLog.debug('_onRawGroupV2MessageCreated skipped: missing group_id or seq');
1326
- return;
1922
+ const currentSlot = slotIsolationKey(String(client._slotId ?? ''));
1923
+ const explicitHeadSlot = this.strictTargetString(head, 'target_slot_id');
1924
+ if (explicitHeadSlot.present && !explicitHeadSlot.valid)
1925
+ return false;
1926
+ const headSlot = explicitHeadSlot.present
1927
+ ? explicitHeadSlot
1928
+ : this.strictBoundString(head, ['slot_id']);
1929
+ if (headSlot.present && (!headSlot.valid || slotIsolationKey(headSlot.value) !== currentSlot))
1930
+ return false;
1931
+ const explicitRowSlot = this.strictTargetString(inline, 'target_slot_id');
1932
+ if (explicitRowSlot.present && (!explicitRowSlot.valid
1933
+ || slotIsolationKey(explicitRowSlot.value) !== currentSlot
1934
+ || (headSlot.present
1935
+ && slotIsolationKey(explicitRowSlot.value) !== slotIsolationKey(headSlot.value))))
1936
+ return false;
1937
+ if (!explicitRowSlot.present) {
1938
+ const rowSlot = this.strictBoundString(inline, ['slot_id']);
1939
+ if (rowSlot.present && (!rowSlot.valid
1940
+ || slotIsolationKey(rowSlot.value) !== currentSlot
1941
+ || (headSlot.present
1942
+ && slotIsolationKey(rowSlot.value) !== slotIsolationKey(headSlot.value))))
1943
+ return false;
1327
1944
  }
1328
- if (!client._v2Session) {
1329
- client._clientLog.debug('_onRawGroupV2MessageCreated skipped: V2 session not initialized');
1330
- return;
1945
+ return true;
1946
+ }
1947
+ inlineV2Envelope(inline) {
1948
+ const raw = inline.envelope_json;
1949
+ if (typeof raw !== 'string' || !raw.trim())
1950
+ return null;
1951
+ try {
1952
+ const envelope = JSON.parse(raw);
1953
+ return isJsonObject(envelope)
1954
+ ? envelope
1955
+ : null;
1331
1956
  }
1332
- const eventKind = String(data.kind ?? '').trim();
1333
- if (eventKind === 'group.online_unread_hint' && !data._online_hint_drained) {
1334
- if (client._sessionOptions?.background_sync === false) {
1335
- client._clientLog.debug(`_onRawGroupV2MessageCreated skipped online unread hint: group=${groupId} background_sync=false`);
1336
- return;
1957
+ catch {
1958
+ return null;
1959
+ }
1960
+ }
1961
+ isInlineV2Row(inline) {
1962
+ return inline.version === 'v2'
1963
+ && typeof inline.envelope_json === 'string'
1964
+ && Boolean(inline.envelope_json.trim());
1965
+ }
1966
+ inlineRecipientMatchesCurrentInstance(envelope) {
1967
+ if (Object.prototype.hasOwnProperty.call(envelope, 'recipients'))
1968
+ return false;
1969
+ if (!isJsonObject(envelope.recipient))
1970
+ return false;
1971
+ const recipient = envelope.recipient;
1972
+ const client = this.runtime.client;
1973
+ if (typeof recipient.aid !== 'string' || typeof recipient.device_id !== 'string'
1974
+ || typeof client._aid !== 'string' || typeof client._deviceId !== 'string'
1975
+ || !recipient.aid || !client._aid || !client._deviceId
1976
+ || recipient.aid !== client._aid)
1977
+ return false;
1978
+ const hasDeliveryDevice = Object.prototype.hasOwnProperty.call(envelope, 'delivery_device_id');
1979
+ const deliveryDevice = envelope.delivery_device_id;
1980
+ if (hasDeliveryDevice
1981
+ && (typeof deliveryDevice !== 'string' || !deliveryDevice || deliveryDevice !== client._deviceId))
1982
+ return false;
1983
+ if (recipient.device_id)
1984
+ return recipient.device_id === client._deviceId;
1985
+ return hasDeliveryDevice;
1986
+ }
1987
+ inlineP2PBindingMatches(head, inline, seq, expectedInlineVersion) {
1988
+ if (typeof head.seq !== 'number' || !Number.isSafeInteger(head.seq) || head.seq <= 0 || head.seq !== seq
1989
+ || typeof inline.seq !== 'number' || !Number.isSafeInteger(inline.seq) || inline.seq <= 0 || inline.seq !== seq
1990
+ || inline.version !== expectedInlineVersion
1991
+ || !this.sameRequiredBinding(head, ['message_id'], inline, ['message_id'])
1992
+ || !this.sameRequiredBinding(head, ['from_aid', 'sender_aid', 'from'], inline, ['from_aid', 'sender_aid', 'from'])
1993
+ || !this.sameRequiredBinding(head, ['sender_device_id'], inline, ['sender_device_id'])
1994
+ || !this.inlineTargetMatchesCurrentInstance(head, inline))
1995
+ return false;
1996
+ const logicalTo = this.consistentBindingValue(head, ['to_aid', 'to']);
1997
+ if (!logicalTo.present || !logicalTo.value)
1998
+ return false;
1999
+ if (expectedInlineVersion === 'v1') {
2000
+ if (Object.prototype.hasOwnProperty.call(inline, 'envelope_json')
2001
+ || Object.prototype.hasOwnProperty.call(inline, 'payload')
2002
+ || !isJsonObject(inline.legacy_v1))
2003
+ return false;
2004
+ const legacy = inline.legacy_v1;
2005
+ const payload = legacy.payload;
2006
+ if (payload === undefined || payload === null)
2007
+ return false;
2008
+ if (isJsonObject(payload)) {
2009
+ const payloadType = String(payload.type ?? '').trim();
2010
+ if (payloadType === 'e2ee.encrypted' || payloadType === 'e2ee.group_encrypted')
2011
+ return false;
1337
2012
  }
1338
- this.enqueueOnlineUnreadHint(data);
1339
- return;
2013
+ const legacyTo = this.strictBoundString(legacy, ['to']);
2014
+ return legacyTo.present && legacyTo.valid && legacyTo.value === logicalTo.value;
2015
+ }
2016
+ if (!this.isInlineV2Row(inline)
2017
+ || Object.prototype.hasOwnProperty.call(inline, 'legacy_v1')
2018
+ || Object.prototype.hasOwnProperty.call(inline, 'payload')
2019
+ || !this.onlyEmptyOptionalStrings(head, ['group_id', 'group_aid'])
2020
+ || !this.onlyEmptyOptionalStrings(inline, ['group_id', 'group_aid']))
2021
+ return false;
2022
+ const envelope = this.inlineV2Envelope(inline);
2023
+ if (!envelope
2024
+ || envelope.type !== 'e2ee.p2p_encrypted'
2025
+ || envelope.version !== 'v2'
2026
+ || !this.onlyEmptyOptionalStrings(envelope, ['group_id'])
2027
+ || !this.inlineRecipientMatchesCurrentInstance(envelope))
2028
+ return false;
2029
+ if (!isJsonObject(envelope.aad))
2030
+ return false;
2031
+ const aad = envelope.aad;
2032
+ if (!this.onlyEmptyOptionalStrings(aad, ['group_id']))
2033
+ return false;
2034
+ const aadMessageId = this.consistentBindingValue(aad, ['message_id']);
2035
+ const aadFrom = this.consistentBindingValue(aad, ['from']);
2036
+ const aadFromDevice = this.consistentBindingValue(aad, ['from_device']);
2037
+ const aadTo = this.consistentBindingValue(aad, ['to']);
2038
+ if (!aadMessageId.present || aadMessageId.value !== this.consistentBindingValue(inline, ['message_id']).value
2039
+ || !aadFrom.present || aadFrom.value !== this.consistentBindingValue(inline, ['from_aid', 'sender_aid', 'from']).value
2040
+ || !aadFromDevice.present || aadFromDevice.value !== this.consistentBindingValue(inline, ['sender_device_id']).value
2041
+ || !aadTo.present)
2042
+ return false;
2043
+ if (aadTo.value === logicalTo.value)
2044
+ return true;
2045
+ const recipient = envelope.recipient;
2046
+ return typeof recipient.role === 'string'
2047
+ && recipient.role.trim() === 'self_sync'
2048
+ && aadTo.value.length > 0
2049
+ && aadFrom.value === String(this.runtime.client._aid ?? '').trim()
2050
+ && logicalTo.value === String(this.runtime.client._aid ?? '').trim();
2051
+ }
2052
+ inlineGroupBindingMatches(head, inline, groupId, seq, expectedInlineVersion) {
2053
+ const headGroupId = this.strictBoundString(head, ['group_id']);
2054
+ const rowGroupId = this.strictBoundString(inline, ['group_id']);
2055
+ const headGroupAid = this.strictBoundString(head, ['group_aid']);
2056
+ const rowGroupAid = this.strictBoundString(inline, ['group_aid']);
2057
+ if (typeof head.seq !== 'number' || !Number.isSafeInteger(head.seq) || head.seq <= 0 || head.seq !== seq
2058
+ || typeof inline.seq !== 'number' || !Number.isSafeInteger(inline.seq) || inline.seq <= 0 || inline.seq !== seq
2059
+ || inline.version !== expectedInlineVersion
2060
+ || !headGroupId.present || !headGroupId.valid || headGroupId.value !== groupId
2061
+ || !rowGroupId.present || !rowGroupId.valid || rowGroupId.value !== groupId
2062
+ || (headGroupAid.present && !headGroupAid.valid)
2063
+ || (rowGroupAid.present && !rowGroupAid.valid)
2064
+ || !this.sameRequiredBinding(head, ['message_id'], inline, ['message_id'])
2065
+ || !this.sameRequiredBinding(head, ['sender_aid', 'from_aid', 'from'], inline, ['from_aid', 'sender_aid', 'from'])
2066
+ || !this.sameRequiredBinding(head, ['sender_device_id'], inline, ['sender_device_id'])
2067
+ || !this.inlineTargetMatchesCurrentInstance(head, inline))
2068
+ return false;
2069
+ if (expectedInlineVersion === 'v1') {
2070
+ if (Object.prototype.hasOwnProperty.call(inline, 'envelope_json')
2071
+ || Object.prototype.hasOwnProperty.call(inline, 'legacy_v1')
2072
+ || !Object.prototype.hasOwnProperty.call(inline, 'payload')
2073
+ || inline.payload === null
2074
+ || inline.payload === undefined)
2075
+ return false;
2076
+ if (isJsonObject(inline.payload)) {
2077
+ const payloadType = String(inline.payload.type ?? '').trim();
2078
+ if (payloadType === 'e2ee.encrypted' || payloadType === 'e2ee.group_encrypted')
2079
+ return false;
2080
+ }
2081
+ return true;
1340
2082
  }
2083
+ if (!this.isInlineV2Row(inline)
2084
+ || Object.prototype.hasOwnProperty.call(inline, 'legacy_v1')
2085
+ || Object.prototype.hasOwnProperty.call(inline, 'payload'))
2086
+ return false;
2087
+ const envelope = this.inlineV2Envelope(inline);
2088
+ const envelopeGroupId = envelope ? this.strictBoundString(envelope, ['group_id']) : null;
2089
+ if (!envelope
2090
+ || envelope.type !== 'e2ee.group_encrypted'
2091
+ || envelope.version !== 'v2'
2092
+ || (envelopeGroupId?.present && (!envelopeGroupId.valid || envelopeGroupId.value !== groupId))
2093
+ || !this.inlineRecipientMatchesCurrentInstance(envelope))
2094
+ return false;
2095
+ if (!isJsonObject(envelope.aad))
2096
+ return false;
2097
+ const aad = envelope.aad;
2098
+ return this.consistentBindingValue(aad, ['message_id']).value === this.consistentBindingValue(inline, ['message_id']).value
2099
+ && this.consistentBindingValue(aad, ['from']).value === this.consistentBindingValue(inline, ['from_aid', 'sender_aid', 'from']).value
2100
+ && this.consistentBindingValue(aad, ['from_device']).value === this.consistentBindingValue(inline, ['sender_device_id']).value
2101
+ && this.consistentBindingValue(aad, ['group_id']).value === groupId;
2102
+ }
2103
+ inlineWindowReady(ns, seq) {
2104
+ if (!ns || !Number.isSafeInteger(seq) || seq <= 0)
2105
+ return false;
2106
+ const expected = seq - 1;
2107
+ const state = this.syncState(ns);
2108
+ if (state.ack !== expected || state.tail !== expected || state.head !== expected)
2109
+ return false;
2110
+ const client = this.runtime.client;
2111
+ // 入口会先登记当前 Push 的 Head,因此 maxSeen==seq 仍是合法的
2112
+ // 单条 inline 快路径;只有已经观察到更高序号时才退回同步路径。
2113
+ if (Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? expected) > seq)
2114
+ return false;
2115
+ const pending = client._pendingOrderedMsgs.get(ns);
2116
+ return !pending || pending.size === 0;
2117
+ }
2118
+ decodedInlineP2PMatches(head, decoded, seq) {
2119
+ const decodedMessageId = this.consistentBindingValue(decoded, ['message_id']);
2120
+ const headMessageId = this.consistentBindingValue(head, ['message_id']);
2121
+ const decodedFrom = this.consistentBindingValue(decoded, ['from', 'from_aid', 'sender_aid']);
2122
+ const headFrom = this.consistentBindingValue(head, ['from_aid', 'sender_aid', 'from']);
2123
+ const decodedTo = this.consistentBindingValue(decoded, ['to', 'to_aid']);
2124
+ const headTo = this.consistentBindingValue(head, ['to_aid', 'to']);
2125
+ return typeof decoded.seq === 'number' && Number.isSafeInteger(decoded.seq) && decoded.seq === seq
2126
+ && decodedMessageId.present && headMessageId.present && decodedMessageId.value === headMessageId.value
2127
+ && decodedFrom.present && headFrom.present && decodedFrom.value === headFrom.value
2128
+ && decodedTo.present && headTo.present && decodedTo.value === headTo.value;
2129
+ }
2130
+ decodedInlineGroupMatches(head, decoded, groupId, seq) {
2131
+ const decodedMessageId = this.consistentBindingValue(decoded, ['message_id']);
2132
+ const headMessageId = this.consistentBindingValue(head, ['message_id']);
2133
+ const decodedFrom = this.consistentBindingValue(decoded, ['from', 'from_aid', 'sender_aid']);
2134
+ const headFrom = this.consistentBindingValue(head, ['sender_aid', 'from_aid', 'from']);
2135
+ const decodedGroupId = this.consistentBindingValue(decoded, ['group_id']);
2136
+ return typeof decoded.seq === 'number' && Number.isSafeInteger(decoded.seq) && decoded.seq === seq
2137
+ && decodedMessageId.present && headMessageId.present && decodedMessageId.value === headMessageId.value
2138
+ && decodedFrom.present && headFrom.present && decodedFrom.value === headFrom.value
2139
+ && decodedGroupId.present && decodedGroupId.value === groupId;
2140
+ }
2141
+ async commitInlineMessage(ns, seq, event, payload, generation) {
2142
+ const client = this.runtime.client;
2143
+ const tracker = client._seqTracker;
2144
+ const snapshot = typeof tracker.snapshotNamespace === 'function'
2145
+ ? tracker.snapshotNamespace(ns)
2146
+ : null;
1341
2147
  try {
1342
- const ns = `group:${groupId}`;
1343
- client._seqTracker.updateMaxSeen(ns, seq);
1344
- const contigBefore = client._seqTracker.getContiguousSeq(ns);
1345
- if (contigBefore === seq || (eventKind === 'group.online_unread_hint' && contigBefore > seq)) {
1346
- client._clientLog.debug(`_onRawGroupV2MessageCreated duplicate push already covered: group=${groupId} seq=${seq}`);
1347
- this.fireGroupV2Ack(groupId, ns, seq, 'group v2 covered push ack');
1348
- return;
2148
+ const published = await this.publishPulledMessage(event, ns, seq, payload);
2149
+ if (!this.isInlineGenerationCurrent(generation))
2150
+ return false;
2151
+ if (!published)
2152
+ return false;
2153
+ const needsPull = Boolean(tracker.onMessageSeq(ns, seq));
2154
+ const committed = this.syncState(ns);
2155
+ if (needsPull || committed.ack !== seq || committed.tail !== seq || committed.head !== seq) {
2156
+ throw new Error('inline realtime cursor did not collapse after consume');
1349
2157
  }
1350
- const afterSeq = client._repairPushContiguousBound(ns, seq, false, '_raw.group.v2.message_created');
1351
- const dedupKey = `group_pull:${ns}`;
1352
- if (client._gapFillDone.has(dedupKey)) {
1353
- client._clientLog.debug(`_onRawGroupV2MessageCreated skipped: dedupKey=${dedupKey} in flight`);
1354
- return;
2158
+ if (!this.isInlineGenerationCurrent(generation))
2159
+ return false;
2160
+ await client._commitSeqTrackerState(ns);
2161
+ if (!this.isInlineGenerationCurrent(generation))
2162
+ return false;
2163
+ return true;
2164
+ }
2165
+ catch (exc) {
2166
+ if (this.isInlineGenerationCurrent(generation) && typeof tracker.restoreNamespaceSnapshot === 'function') {
2167
+ tracker.restoreNamespaceSnapshot(ns, snapshot);
2168
+ }
2169
+ client._clientLog.debug(`inline realtime consume rolled back: ns=${ns} seq=${seq} err=${formatDeliveryError(exc)}`);
2170
+ return false;
2171
+ }
2172
+ }
2173
+ async tryConsumeInlineP2P(head, inline, ns, seq, expectedInlineVersion, generation) {
2174
+ if (!this.isInlineGenerationCurrent(generation)
2175
+ || !this.inlineP2PBindingMatches(head, inline, seq, expectedInlineVersion) || !this.inlineWindowReady(ns, seq))
2176
+ return false;
2177
+ const client = this.runtime.client;
2178
+ try {
2179
+ const result = await client._rpcPipeline.tryRunInlineRealtime(ns, seq, async () => {
2180
+ if (!this.isInlineGenerationCurrent(generation)
2181
+ || !this.inlineWindowReady(ns, seq)
2182
+ || !this.inlineP2PBindingMatches(head, inline, seq, expectedInlineVersion)
2183
+ || !this.reservePendingP2PInlineAck(ns, seq, inline, generation))
2184
+ return false;
2185
+ let committed = false;
2186
+ try {
2187
+ const decoded = await client._v2E2EE.decodeInlineP2PMessage(inline);
2188
+ if (!this.isInlineGenerationCurrent(generation)
2189
+ || !decoded || !this.decodedInlineP2PMatches(head, decoded, seq)
2190
+ || !this.inlineWindowReady(ns, seq))
2191
+ return false;
2192
+ const appEvent = this.p2pAppEventForMessage(decoded);
2193
+ if (!await this.commitInlineMessage(ns, seq, appEvent.event, appEvent.payload, generation))
2194
+ return false;
2195
+ if (!this.isInlineGenerationCurrent(generation))
2196
+ return false;
2197
+ committed = this.markPendingP2PInlineAckCommitted(ns, seq, generation);
2198
+ return committed;
2199
+ }
2200
+ finally {
2201
+ if (!committed && this.isInlineGenerationCurrent(generation))
2202
+ this.dropPendingP2PInlineAck(ns, seq, generation);
2203
+ }
2204
+ });
2205
+ const consumed = this.isInlineGenerationCurrent(generation) && result.acquired === true && result.value === true;
2206
+ return consumed;
2207
+ }
2208
+ catch (exc) {
2209
+ client._clientLog.debug(`inline P2P consume failed: seq=${seq} err=${formatDeliveryError(exc)}`);
2210
+ return false;
2211
+ }
2212
+ }
2213
+ async tryConsumeInlineGroup(head, inline, groupId, ns, seq, expectedInlineVersion, generation) {
2214
+ if (!this.isInlineGenerationCurrent(generation)
2215
+ || !this.inlineGroupBindingMatches(head, inline, groupId, seq, expectedInlineVersion) || !this.inlineWindowReady(ns, seq))
2216
+ return false;
2217
+ const client = this.runtime.client;
2218
+ const groupAid = typeof head.group_aid === 'string' && head.group_aid.trim()
2219
+ ? head.group_aid.trim()
2220
+ : groupId;
2221
+ try {
2222
+ const result = await client._rpcPipeline.tryRunInlineRealtime(ns, seq, async () => {
2223
+ if (!this.isInlineGenerationCurrent(generation)
2224
+ || !this.inlineWindowReady(ns, seq)
2225
+ || !this.inlineGroupBindingMatches(head, inline, groupId, seq, expectedInlineVersion)
2226
+ || !this.reservePendingGroupInlineAck(ns, seq, inline, groupAid, groupId, generation))
2227
+ return false;
2228
+ let committed = false;
2229
+ try {
2230
+ const decoded = await client._v2E2EE.decodeInlineGroupMessage(inline, groupId, groupAid);
2231
+ if (!this.isInlineGenerationCurrent(generation)
2232
+ || !decoded || !this.decodedInlineGroupMatches(head, decoded, groupId, seq)
2233
+ || !this.inlineWindowReady(ns, seq))
2234
+ return false;
2235
+ if (!await this.commitInlineMessage(ns, seq, 'group.message_created', decoded, generation))
2236
+ return false;
2237
+ if (!this.isInlineGenerationCurrent(generation))
2238
+ return false;
2239
+ committed = this.markPendingGroupInlineAckCommitted(ns, seq, generation);
2240
+ return committed;
2241
+ }
2242
+ finally {
2243
+ if (!committed && this.isInlineGenerationCurrent(generation))
2244
+ this.dropPendingGroupInlineAck(ns, seq, generation);
2245
+ }
2246
+ });
2247
+ const consumed = this.isInlineGenerationCurrent(generation) && result.acquired === true && result.value === true;
2248
+ return consumed;
2249
+ }
2250
+ catch (exc) {
2251
+ client._clientLog.debug(`inline Group consume failed: group=${groupId} seq=${seq} err=${formatDeliveryError(exc)}`);
2252
+ return false;
2253
+ }
2254
+ }
2255
+ hasOutstandingRealtimeGap(ns) {
2256
+ const maxSeenSeq = this.runtime.client._seqTracker.getMaxSeenSeq(ns);
2257
+ return Number.isSafeInteger(maxSeenSeq) && maxSeenSeq > this.syncState(ns).ack;
2258
+ }
2259
+ scheduleP2PRealtimeFullGapFill(ns) {
2260
+ const client = this.runtime.client;
2261
+ const currentNs = client._aid ? `p2p:${client._aid}` : '';
2262
+ if (client._sessionOptions?.background_sync === false
2263
+ || client.state !== ConnectionState.READY || client._closing
2264
+ || currentNs !== ns || !this.hasOutstandingRealtimeGap(ns))
2265
+ return;
2266
+ const scheduleKey = `p2p_realtime_full_gap:${ns}`;
2267
+ if (client._gapFillDone.has(scheduleKey))
2268
+ return;
2269
+ client._gapFillDone.add(scheduleKey);
2270
+ client._safeAsync(Promise.resolve().then(async () => {
2271
+ try {
2272
+ const liveNs = client._aid ? `p2p:${client._aid}` : '';
2273
+ if (client._sessionOptions?.background_sync === false
2274
+ || client.state !== ConnectionState.READY || client._closing
2275
+ || liveNs !== ns || !this.hasOutstandingRealtimeGap(ns))
2276
+ return;
2277
+ await this.fillP2pGap();
2278
+ }
2279
+ finally {
2280
+ client._gapFillDone.delete(scheduleKey);
1355
2281
  }
1356
- client._gapFillDone.add(dedupKey);
2282
+ }));
2283
+ }
2284
+ scheduleGroupRealtimeFullGapFill(groupId, ns) {
2285
+ const client = this.runtime.client;
2286
+ groupId = String(groupId ?? '').trim();
2287
+ if (!groupId || client._sessionOptions?.background_sync === false
2288
+ || client.state !== ConnectionState.READY || client._closing
2289
+ || !this.hasOutstandingRealtimeGap(ns))
2290
+ return;
2291
+ const scheduleKey = `group_realtime_full_gap:${ns}`;
2292
+ if (client._gapFillDone.has(scheduleKey))
2293
+ return;
2294
+ client._gapFillDone.add(scheduleKey);
2295
+ client._safeAsync(Promise.resolve().then(async () => {
1357
2296
  try {
1358
- client._clientLog.debug(`_onRawGroupV2MessageCreated -> group.v2.pull group=${groupId} after_seq=${afterSeq}`);
1359
- const messages = await client._withBackgroundRpc(() => client._pullGroupV2(groupId, afterSeq, 50));
1360
- client._clientLog.debug(`_onRawGroupV2MessageCreated pulled ${messages.length} msgs for group=${groupId}`);
2297
+ if (client._sessionOptions?.background_sync === false
2298
+ || client.state !== ConnectionState.READY || client._closing
2299
+ || !this.hasOutstandingRealtimeGap(ns))
2300
+ return;
2301
+ await this.fillGroupGap(groupId);
1361
2302
  }
1362
2303
  finally {
1363
- client._gapFillDone.delete(dedupKey);
2304
+ client._gapFillDone.delete(scheduleKey);
1364
2305
  }
2306
+ }));
2307
+ }
2308
+ async runP2PRealtimeSync(ns, pushSeq, pageLimit = 50, forceTail = false) {
2309
+ const client = this.runtime.client;
2310
+ const order = client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit);
2311
+ let result = { messages: [], raw_count: 0 };
2312
+ let tailCompleted = false;
2313
+ const run = async (tail, background, maxPages, forceForward = false, bypassCoveredHead = false) => {
2314
+ const live = this.syncState(ns);
2315
+ if (tail && !bypassCoveredHead && pushSeq <= live.head)
2316
+ return { messages: [], raw_count: 0 };
2317
+ if (!tail && !forceForward && live.ack >= live.tail - 1)
2318
+ return { messages: [], raw_count: 0 };
2319
+ const request = { ...(tail ? { window_mode: 'tail' } : {}), after_seq: live.ack, limit: pageLimit };
2320
+ const key = client._rpcPipeline.pullGateKeyForCall('message.pull', request);
2321
+ return await client._rpcPipeline.runPullSerialized(key, async () => {
2322
+ if (tail) {
2323
+ const tailResult = await client._v2E2EE.pullV2TailInternal({
2324
+ window_mode: 'tail', after_seq: live.ack, limit: pageLimit,
2325
+ });
2326
+ tailCompleted = true;
2327
+ return tailResult;
2328
+ }
2329
+ const messages = await client._pullV2(live.ack, pageLimit, {
2330
+ gateLocked: true,
2331
+ ...(maxPages ? { maxPages } : {}),
2332
+ });
2333
+ return { messages, raw_count: messages.length };
2334
+ }, background);
2335
+ };
2336
+ if (forceTail || order[0] === 'tail')
2337
+ result = await run(true, false, undefined, false, forceTail);
2338
+ const state = this.syncState(ns);
2339
+ // Tail 的连续性证明只覆盖其返回的 head。Tail 进行期间到达的新
2340
+ // Push 不能再次进入 realtime planner(那会再做一次 Tail);以已经
2341
+ // 落盘的 ack 为起点,至多补一页普通 Forward 即可恢复该小窗口。
2342
+ const tailMissedDuringPull = tailCompleted
2343
+ && Number(client._seqTracker.getMaxSeenSeq(ns) || 0) > state.head;
2344
+ let boundedForwardForMissedHead = false;
2345
+ if (client._sessionOptions?.background_sync !== false
2346
+ && (order[0] === 'forward'
2347
+ || (tailCompleted && (state.ack < state.tail - 1 || tailMissedDuringPull)))) {
2348
+ result = await run(false, true, order[0] === 'tail' ? 1 : undefined, tailMissedDuringPull);
2349
+ boundedForwardForMissedHead = order[0] === 'tail' && tailMissedDuringPull;
2350
+ }
2351
+ if (boundedForwardForMissedHead)
2352
+ this.scheduleP2PRealtimeFullGapFill(ns);
2353
+ if (tailCompleted)
2354
+ this.markRealtimeTailCompleted(result);
2355
+ return result;
2356
+ }
2357
+ async runGroupRealtimeSync(groupId, ns, pushSeq, pageLimit = 50, forceTail = false) {
2358
+ const client = this.runtime.client;
2359
+ const order = client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit);
2360
+ let result = { messages: [], raw_count: 0 };
2361
+ let tailCompleted = false;
2362
+ const run = async (tail, background, maxPages, forceForward = false, bypassCoveredHead = false) => {
2363
+ const live = this.syncState(ns);
2364
+ if (tail && !bypassCoveredHead && pushSeq <= live.head)
2365
+ return { messages: [], raw_count: 0 };
2366
+ if (!tail && !forceForward && live.ack >= live.tail - 1)
2367
+ return { messages: [], raw_count: 0 };
2368
+ const request = { group_id: groupId, ...(tail ? { window_mode: 'tail' } : {}), after_seq: live.ack, limit: pageLimit };
2369
+ const key = client._rpcPipeline.pullGateKeyForCall('group.pull', request);
2370
+ return await client._rpcPipeline.runPullSerialized(key, async () => {
2371
+ if (tail) {
2372
+ const tailResult = await client._v2E2EE.pullGroupV2TailInternal({
2373
+ group_id: groupId, window_mode: 'tail', after_seq: live.ack, limit: pageLimit,
2374
+ });
2375
+ tailCompleted = true;
2376
+ return tailResult;
2377
+ }
2378
+ const messages = await client._pullGroupV2(groupId, live.ack, pageLimit, {
2379
+ gateLocked: true,
2380
+ ...(maxPages ? { maxPages } : {}),
2381
+ });
2382
+ return { messages, raw_count: messages.length };
2383
+ }, background);
2384
+ };
2385
+ if (forceTail || order[0] === 'tail')
2386
+ result = await run(true, false, undefined, false, forceTail);
2387
+ const state = this.syncState(ns);
2388
+ // 与 P2P 相同:真实完成 Tail 后,至多用一页 Forward 收敛 Tail
2389
+ // 期间新到达的序号;绝不重新规划或再执行 Tail。
2390
+ const tailMissedDuringPull = tailCompleted
2391
+ && Number(client._seqTracker.getMaxSeenSeq(ns) || 0) > state.head;
2392
+ let boundedForwardForMissedHead = false;
2393
+ if (client._sessionOptions?.background_sync !== false
2394
+ && (order[0] === 'forward'
2395
+ || (tailCompleted && (state.ack < state.tail - 1 || tailMissedDuringPull)))) {
2396
+ result = await run(false, true, order[0] === 'tail' ? 1 : undefined, tailMissedDuringPull);
2397
+ boundedForwardForMissedHead = order[0] === 'tail' && tailMissedDuringPull;
2398
+ }
2399
+ if (boundedForwardForMissedHead)
2400
+ this.scheduleGroupRealtimeFullGapFill(groupId, ns);
2401
+ if (tailCompleted)
2402
+ this.markRealtimeTailCompleted(result);
2403
+ return result;
2404
+ }
2405
+ async runGroupOnlineUnreadHintForward(groupId, ns, pageLimit = 50) {
2406
+ const client = this.runtime.client;
2407
+ const request = { group_id: groupId, after_seq: this.syncState(ns).ack, limit: pageLimit, max_pages: 1 };
2408
+ const key = client._rpcPipeline.pullGateKeyForCall('group.pull', request);
2409
+ return await client._rpcPipeline.runPullSerialized(key, async () => {
2410
+ const live = this.syncState(ns);
2411
+ const messages = await client._pullGroupV2(groupId, live.ack, pageLimit, {
2412
+ gateLocked: true, maxPages: 1,
2413
+ });
2414
+ return { messages, raw_count: messages.length };
2415
+ }, true);
2416
+ }
2417
+ async recoverInvalidP2PRealtimeHead(ns, rawSeq) {
2418
+ const client = this.runtime.client;
2419
+ client._clientLog.warn(`V2 P2P inline Head has invalid seq; forcing authoritative Tail: seq=${String(rawSeq)}`);
2420
+ if (!this.beginRealtimeSync(ns))
2421
+ return;
2422
+ try {
2423
+ await this.runP2PRealtimeSync(ns, this.syncState(ns).head, 50, true);
1365
2424
  }
1366
2425
  catch (exc) {
1367
- client._clientLog.warn(`_onRawGroupV2MessageCreated pull failed group=${groupId}: ${String(exc)}`);
2426
+ client._clientLog.warn(`V2 P2P invalid-seq Tail failed: ${formatDeliveryError(exc)}`);
1368
2427
  }
1369
2428
  finally {
1370
- client._clientLog.debug(`_onRawGroupV2MessageCreated exit: elapsed=${Date.now() - tStart}ms`);
2429
+ this.endRealtimeSync(ns);
1371
2430
  }
1372
2431
  }
1373
- async onV2PushNotification(data) {
2432
+ async recoverInvalidGroupRealtimeHead(groupId, ns, rawSeq) {
1374
2433
  const client = this.runtime.client;
1375
- if (!client._v2Session)
2434
+ client._clientLog.warn(`V2 Group inline Head has invalid seq; forcing authoritative Tail: group=${groupId} seq=${String(rawSeq)}`);
2435
+ if (!this.beginRealtimeSync(ns))
1376
2436
  return;
1377
- const pushSeq = isJsonObject(data) ? Number(data.seq ?? 0) || 0 : 0;
1378
- const pushFrom = isJsonObject(data) ? String(data.from_aid ?? '') : '';
1379
- const pushMsgId = isJsonObject(data) ? String(data.message_id ?? '') : '';
1380
- const envelopeJson = isJsonObject(data) ? data.envelope_json : undefined;
1381
- const hasPayload = !!envelopeJson;
1382
- const ns = client._aid ? `p2p:${client._aid}` : '';
1383
- let contigBefore = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
1384
- client._clientLog.debug(`_onV2PushNotification: push_seq=${pushSeq || 'null'} push_from=${pushFrom} push_msg_id=${pushMsgId} has_payload=${hasPayload} contiguous_seq=${contigBefore}`);
1385
- if (pushSeq > 0 && ns) {
1386
- client._seqTracker.updateMaxSeen(ns, pushSeq);
1387
- if (contigBefore === pushSeq) {
1388
- client._clientLog.debug(`_onV2PushNotification: push seq=${pushSeq} already covered by contiguous_seq=${contigBefore}, ignore duplicate push`);
2437
+ try {
2438
+ await this.runGroupRealtimeSync(groupId, ns, this.syncState(ns).head, 50, true);
2439
+ }
2440
+ catch (exc) {
2441
+ client._clientLog.warn(`V2 Group invalid-seq Tail failed: group=${groupId} err=${formatDeliveryError(exc)}`);
2442
+ }
2443
+ finally {
2444
+ this.endRealtimeSync(ns);
2445
+ }
2446
+ }
2447
+ async onRawGroupV2MessageCreated(data) {
2448
+ const client = this.runtime.client;
2449
+ const row = isJsonObject(data) ? data : null;
2450
+ if (!row)
2451
+ return;
2452
+ const groupId = typeof row.group_id === 'string' ? row.group_id.trim() : '';
2453
+ if (!groupId)
2454
+ return;
2455
+ const seq = typeof row.seq === 'number' ? row.seq : 0;
2456
+ const eventKind = String(row.kind ?? '').trim();
2457
+ const ns = this.resolvePendingGroupInlineAckNamespace(`group:${groupId}`);
2458
+ if (client._v2Session && eventKind !== 'group.online_unread_hint'
2459
+ && Number.isSafeInteger(seq) && seq > 0) {
2460
+ // 必须在进入 namespace 队列前登记,才能让在途 Tail 看见后来 Push。
2461
+ client._seqTracker.updateMaxSeen(ns, seq);
2462
+ }
2463
+ const hasInlineObject = isJsonObject(row.inline_message)
2464
+ && eventKind !== 'group.online_unread_hint';
2465
+ const inlineGeneration = hasInlineObject ? this.captureInlineGeneration() : undefined;
2466
+ const operation = () => this.processGroupV2MessageCreated(data, 'v2', inlineGeneration);
2467
+ if (hasInlineObject)
2468
+ await this.runPushProcessSerialized(ns, operation, inlineGeneration);
2469
+ else
2470
+ await operation();
2471
+ }
2472
+ async processGroupV2MessageCreated(data, expectedInlineVersion, queuedInlineGeneration) {
2473
+ if (queuedInlineGeneration !== undefined && !this.isInlineGenerationCurrent(queuedInlineGeneration))
2474
+ return;
2475
+ const client = this.runtime.client;
2476
+ if (!isJsonObject(data) || !client._v2Session)
2477
+ return;
2478
+ const groupId = typeof data.group_id === 'string' ? data.group_id.trim() : '';
2479
+ const seq = positiveSafeSequenceHint(data.seq);
2480
+ const strictSeq = typeof data.seq === 'number' && Number.isSafeInteger(data.seq) && data.seq > 0;
2481
+ if (!groupId)
2482
+ return;
2483
+ const eventKind = String(data.kind ?? '').trim();
2484
+ // Hint 是恢复信号而不是消息 Head:不得消费 inline、推进水位或直接 ACK。
2485
+ if (eventKind === 'group.online_unread_hint') {
2486
+ if (!data._online_hint_drained) {
2487
+ if (client._sessionOptions?.background_sync === false)
2488
+ return;
2489
+ this.enqueueOnlineUnreadHint(data);
1389
2490
  return;
1390
2491
  }
1391
- contigBefore = client._repairPushContiguousBound(ns, pushSeq, hasPayload, '_raw.peer.v2.message_received');
1392
- }
1393
- if (hasPayload && pushSeq > 0 && ns) {
2492
+ if (client._sessionOptions?.background_sync === false)
2493
+ return;
1394
2494
  try {
1395
- const decrypted = await client._decryptV2PushMessage(data);
1396
- if (decrypted) {
1397
- const needPull = client._seqTracker.onMessageSeq(ns, pushSeq);
1398
- const published = await client._publishOrderedMessage('message.received', ns, pushSeq, decrypted);
1399
- const newContig = client._seqTracker.getContiguousSeq(ns);
1400
- if (newContig !== contigBefore) {
1401
- this.persistSeq(ns);
1402
- }
1403
- if (newContig > 0 && newContig !== contigBefore) {
1404
- const ackSeq = this.clampAckSeq('message.v2.ack', 'up_to_seq', ns, newContig);
1405
- try {
1406
- await client._callRawV2Rpc('message.v2.ack', { up_to_seq: ackSeq, _rpc_background: true });
1407
- }
1408
- catch (e) {
1409
- client._clientLog.debug(`V2 P2P push-ack failed: ${e}`);
1410
- }
1411
- }
1412
- client._clientLog.debug(`_onV2PushNotification: push 带 payload 解密成功, contiguous_seq=${contigBefore}->${newContig} push_seq=${pushSeq}`);
1413
- if (!needPull && (published || newContig >= pushSeq || pushSeq <= contigBefore)) {
1414
- return;
1415
- }
1416
- client._clientLog.debug(`_onV2PushNotification: payload push seq=${pushSeq} 因空洞挂起,继续 pull 补齐 after_seq=${newContig}`);
1417
- }
2495
+ // 只从已提交 Ack 做一页普通 Forward,不能由 planner 把 hint 当 Tail 证据。
2496
+ await this.runGroupOnlineUnreadHintForward(groupId, `group:${groupId}`);
1418
2497
  }
1419
2498
  catch (exc) {
1420
- client._clientLog.debug(`_onV2PushNotification: push payload 解密失败, fallback to pull: ${exc}`);
2499
+ client._clientLog.warn(`group online unread hint recovery pull failed: group=${groupId} err=${formatDeliveryError(exc)}`);
1421
2500
  }
2501
+ return;
1422
2502
  }
1423
- if (pushSeq > 0 && ns) {
1424
- client._clientLog.debug(`_onV2PushNotification: 纯通知 push_seq=${pushSeq} > contiguous_seq=${contigBefore}, 触发 pull(after_seq=${contigBefore})`);
2503
+ const ns = this.resolvePendingGroupInlineAckNamespace(`group:${groupId}`);
2504
+ const inlinePresent = Object.prototype.hasOwnProperty.call(data, 'inline_message');
2505
+ if (seq <= 0) {
2506
+ if (inlinePresent)
2507
+ await this.recoverInvalidGroupRealtimeHead(groupId, ns, data.seq);
2508
+ return;
1425
2509
  }
1426
- if (client._v2PullInflight) {
1427
- this.runtime.delivery.setV2PullPending(true);
2510
+ const inline = this.inlineMessage(data);
2511
+ const inlineMatches = inline !== null
2512
+ && this.inlineGroupBindingMatches(data, inline, groupId, seq, expectedInlineVersion);
2513
+ const pendingInline = this.pendingGroupInlineAcks?.get(ns);
2514
+ const pendingConflict = inline !== null && pendingInline !== undefined && pendingInline.seq !== seq;
2515
+ const inlineGeneration = inlineMatches ? (queuedInlineGeneration ?? this.inlineGeneration) : -1;
2516
+ if (inlineMatches && !pendingConflict
2517
+ && await this.tryConsumeInlineGroup(data, inline, groupId, ns, seq, expectedInlineVersion, inlineGeneration)) {
2518
+ const pending = this.pendingGroupInlineAcks?.get(ns);
2519
+ if (pending)
2520
+ this.retryPendingGroupInlineAck(ns, pending.seq, 'inline Group ack');
1428
2521
  return;
1429
2522
  }
1430
- this.runtime.delivery.setV2PullInflight(true);
1431
- const dedupKey = `p2p_pull:${ns}`;
1432
- client._gapFillDone.add(dedupKey);
2523
+ if (inlineMatches && !this.isInlineGenerationCurrent(inlineGeneration))
2524
+ return;
2525
+ const before = this.syncState(ns);
2526
+ // 无正文的旧服务端 Head 可把 JSON-safe 数字字符串作为恢复 hint;
2527
+ // inline 绑定仍要求原始 seq 是严格 number,避免类型归一化后消费正文。
2528
+ if (strictSeq || !inlinePresent)
2529
+ client._seqTracker.updateMaxSeen(ns, seq);
2530
+ if ((!inlinePresent || inlineMatches) && !pendingConflict) {
2531
+ if (seq <= before.head && before.ack >= before.tail - 1) {
2532
+ const canonicalNs = this.resolvePendingGroupInlineAckNamespace(ns);
2533
+ const pending = this.pendingGroupInlineAcks?.get(canonicalNs);
2534
+ if (pending && seq >= pending.seq) {
2535
+ this.retryPendingGroupInlineAck(canonicalNs, pending.seq, 'covered inline Group ack', seq);
2536
+ }
2537
+ else {
2538
+ this.fireGroupV2Ack(groupId, ns, seq, 'group v2 covered push ack', inlineMatches ? inlineGeneration : null);
2539
+ }
2540
+ return;
2541
+ }
2542
+ }
2543
+ if (!this.beginRealtimeSync(ns))
2544
+ return;
1433
2545
  try {
1434
- do {
1435
- this.runtime.delivery.setV2PullPending(false);
1436
- await client._withBackgroundRpc(() => client._pullV2());
1437
- const newContig = ns ? client._seqTracker.getContiguousSeq(ns) : -1;
1438
- client._clientLog.debug(`_onV2PushNotification pull done: contiguous_seq=${contigBefore}->${newContig} (push_seq=${pushSeq || 'null'})`);
1439
- } while (client._v2PullPending);
2546
+ // namespace 串行区必须覆盖 Tail 与其后至多一次 Forward 的完整周期。
2547
+ await this.runGroupRealtimeSync(groupId, ns, seq, 50, inlinePresent && (!inlineMatches || pendingConflict));
1440
2548
  }
1441
2549
  catch (exc) {
1442
- const newContig = ns ? client._seqTracker.getContiguousSeq(ns) : -1;
1443
- client._clientLog.warn(`V2 push auto-pull failed: contiguous_seq=${contigBefore}->${newContig} err=${exc}`);
2550
+ client._clientLog.warn(`V2 group push auto-sync failed: group=${groupId} err=${formatDeliveryError(exc)}`);
1444
2551
  }
1445
2552
  finally {
1446
- this.runtime.delivery.setV2PullInflight(false);
1447
- client._gapFillDone.delete(dedupKey);
2553
+ this.endRealtimeSync(ns);
2554
+ }
2555
+ }
2556
+ async onV2PushNotification(data) {
2557
+ const client = this.runtime.client;
2558
+ const ns = client._aid ? `p2p:${client._aid}` : '';
2559
+ if (!ns)
2560
+ return;
2561
+ const pushSeq = isJsonObject(data) && typeof data.seq === 'number' ? data.seq : 0;
2562
+ if (client._v2Session && Number.isSafeInteger(pushSeq) && pushSeq > 0) {
2563
+ // 与 Group 相同,在排队前暴露更高 Head 给当前在途 Tail。
2564
+ client._seqTracker.updateMaxSeen(ns, pushSeq);
2565
+ }
2566
+ const hasInlineObject = isJsonObject(data)
2567
+ && isJsonObject(data.inline_message);
2568
+ const inlineGeneration = hasInlineObject ? this.captureInlineGeneration() : undefined;
2569
+ const operation = () => this.processV2P2PNotification(data, 'v2', inlineGeneration);
2570
+ if (hasInlineObject)
2571
+ await this.runPushProcessSerialized(ns, operation, inlineGeneration);
2572
+ else
2573
+ await operation();
2574
+ }
2575
+ async processV2P2PNotification(data, expectedInlineVersion, queuedInlineGeneration) {
2576
+ if (queuedInlineGeneration !== undefined && !this.isInlineGenerationCurrent(queuedInlineGeneration))
2577
+ return;
2578
+ const client = this.runtime.client;
2579
+ if (!client._v2Session)
2580
+ return;
2581
+ const pushSeq = isJsonObject(data) ? positiveSafeSequenceHint(data.seq) : 0;
2582
+ const strictSeq = isJsonObject(data)
2583
+ && typeof data.seq === 'number' && Number.isSafeInteger(data.seq) && data.seq > 0;
2584
+ const ns = client._aid ? `p2p:${client._aid}` : '';
2585
+ if (!ns)
2586
+ return;
2587
+ const inlinePresent = isJsonObject(data) && Object.prototype.hasOwnProperty.call(data, 'inline_message');
2588
+ if (pushSeq <= 0) {
2589
+ if (inlinePresent)
2590
+ await this.recoverInvalidP2PRealtimeHead(ns, isJsonObject(data) ? data.seq : undefined);
2591
+ return;
2592
+ }
2593
+ const inline = isJsonObject(data) ? this.inlineMessage(data) : null;
2594
+ const inlineMatches = inline !== null
2595
+ && this.inlineP2PBindingMatches(data, inline, pushSeq, expectedInlineVersion);
2596
+ const pendingInline = this.pendingP2PInlineAcks?.get(ns);
2597
+ const pendingConflict = inline !== null && pendingInline !== undefined && pendingInline.seq !== pushSeq;
2598
+ const inlineGeneration = inlineMatches ? (queuedInlineGeneration ?? this.inlineGeneration) : -1;
2599
+ if (inlineMatches && !pendingConflict
2600
+ && await this.tryConsumeInlineP2P(data, inline, ns, pushSeq, expectedInlineVersion, inlineGeneration)) {
2601
+ this.retryPendingP2PInlineAck(ns, pushSeq, 'inline P2P ack');
2602
+ return;
2603
+ }
2604
+ if (inlineMatches && !this.isInlineGenerationCurrent(inlineGeneration))
2605
+ return;
2606
+ const before = this.syncState(ns);
2607
+ if (strictSeq || !inlinePresent)
2608
+ client._seqTracker.updateMaxSeen(ns, pushSeq);
2609
+ if ((!inlinePresent || inlineMatches) && !pendingConflict) {
2610
+ if (pushSeq <= before.head && before.ack >= before.tail - 1) {
2611
+ const pending = this.pendingP2PInlineAcks?.get(ns);
2612
+ if (pending && pushSeq >= pending.seq) {
2613
+ this.retryPendingP2PInlineAck(ns, pending.seq, 'covered inline P2P ack', pushSeq);
2614
+ }
2615
+ else if (inlineMatches) {
2616
+ this.fireP2PV2Ack(pushSeq, 'P2P inline covered push ack', inlineGeneration);
2617
+ }
2618
+ return;
2619
+ }
2620
+ }
2621
+ if (!this.beginRealtimeSync(ns))
2622
+ return;
2623
+ try {
2624
+ await this.runP2PRealtimeSync(ns, pushSeq, 50, inlinePresent && (!inlineMatches || pendingConflict));
2625
+ }
2626
+ catch (exc) {
2627
+ client._clientLog.warn(`V2 P2P push auto-sync failed: ${formatDeliveryError(exc)}`);
2628
+ }
2629
+ finally {
2630
+ this.endRealtimeSync(ns);
1448
2631
  }
1449
2632
  }
1450
2633
  async restoreSeqTrackerState() {
@@ -1458,6 +2641,17 @@ export class MessageDeliveryEngine {
1458
2641
  const deviceId = client._deviceId;
1459
2642
  const slotId = client._slotId;
1460
2643
  try {
2644
+ const loadSync = client._tokenStore.loadAllSyncStates?.bind(client._tokenStore);
2645
+ if (typeof loadSync === 'function' && typeof client._seqTracker.restoreSyncState === 'function') {
2646
+ let state = await loadSync(aid, deviceId, slotId);
2647
+ if (client._seqTrackerContext !== context)
2648
+ return;
2649
+ if (state && typeof state === 'object' && Object.keys(state).length > 0) {
2650
+ state = await this.migrateSeqStateGroupIds(state);
2651
+ client._seqTracker.restoreSyncState(state);
2652
+ return;
2653
+ }
2654
+ }
1461
2655
  const loadAll = client._tokenStore.loadAllSeqs?.bind(client._tokenStore);
1462
2656
  if (typeof loadAll === 'function') {
1463
2657
  let state = await loadAll(aid, deviceId, slotId);
@@ -1494,71 +2688,7 @@ export class MessageDeliveryEngine {
1494
2688
  }
1495
2689
  }
1496
2690
  async migrateSeqStateGroupIds(state) {
1497
- const client = this.runtime.client;
1498
- if (!state || Object.keys(state).length === 0)
1499
- return state;
1500
- const renameMap = {};
1501
- for (const ns of Object.keys(state)) {
1502
- for (const prefix of ['group_event:', 'group_msg:']) {
1503
- if (ns.startsWith(prefix)) {
1504
- const oldGid = ns.slice(prefix.length);
1505
- const newGid = normalizeGroupId(oldGid);
1506
- if (newGid && newGid !== oldGid) {
1507
- renameMap[ns] = `${prefix}${newGid}`;
1508
- }
1509
- break;
1510
- }
1511
- }
1512
- }
1513
- if (Object.keys(renameMap).length === 0)
1514
- return state;
1515
- const newState = { ...state };
1516
- for (const [oldNs, newNs] of Object.entries(renameMap)) {
1517
- const oldVal = Number(newState[oldNs] ?? 0);
1518
- const curVal = Number(newState[newNs] ?? 0);
1519
- delete newState[oldNs];
1520
- newState[newNs] = Math.max(oldVal, curVal);
1521
- }
1522
- if (!client._aid)
1523
- return newState;
1524
- const aid = client._aid;
1525
- const deviceId = client._deviceId;
1526
- const slotId = client._slotId;
1527
- const saver = client._tokenStore.saveSeq?.bind(client._tokenStore);
1528
- const deleter = client._tokenStore.deleteSeq?.bind(client._tokenStore);
1529
- if (typeof saver === 'function') {
1530
- for (const [oldNs, newNs] of Object.entries(renameMap)) {
1531
- if (typeof deleter === 'function') {
1532
- try {
1533
- await deleter(aid, deviceId, slotId, oldNs);
1534
- }
1535
- catch (e) {
1536
- client._dispatcher.publish('seq_tracker.persist_error', {
1537
- phase: 'migrate_delete',
1538
- aid,
1539
- device_id: deviceId,
1540
- slot_id: slotId,
1541
- ns: oldNs,
1542
- error: String(e),
1543
- }).catch(() => { });
1544
- }
1545
- }
1546
- try {
1547
- await saver(aid, deviceId, slotId, newNs, newState[newNs]);
1548
- }
1549
- catch (e) {
1550
- client._dispatcher.publish('seq_tracker.persist_error', {
1551
- phase: 'migrate_save',
1552
- aid,
1553
- device_id: deviceId,
1554
- slot_id: slotId,
1555
- ns: newNs,
1556
- error: String(e),
1557
- }).catch(() => { });
1558
- }
1559
- }
1560
- }
1561
- return newState;
2691
+ return state;
1562
2692
  }
1563
2693
  async saveSeqTrackerState() {
1564
2694
  const client = this.runtime.client;
@@ -1567,7 +2697,9 @@ export class MessageDeliveryEngine {
1567
2697
  await this.flushSeqTrackerPending();
1568
2698
  return;
1569
2699
  }
1570
- const state = client._seqTracker.exportState();
2700
+ const state = typeof client._seqTracker.exportSyncState === 'function'
2701
+ ? client._seqTracker.exportSyncState()
2702
+ : Object.fromEntries(Object.entries(client._seqTracker.exportState()).map(([ns, seq]) => [ns, { ack: Number(seq), tail: Number(seq), head: Number(seq) }]));
1571
2703
  const pending = this.seqTrackerPending();
1572
2704
  if (pending.size > 0 && client._seqTrackerPendingPersistContext === context) {
1573
2705
  const liveNamespaces = new Set(Object.keys(state));
@@ -1595,9 +2727,28 @@ export class MessageDeliveryEngine {
1595
2727
  if (forceSeq !== undefined) {
1596
2728
  client._seqTracker.forceContiguousSeq(ns, seq);
1597
2729
  }
1598
- this.mergeSeqTrackerPending(context, { [ns]: seq });
2730
+ const state = typeof client._seqTracker.getSyncState === 'function'
2731
+ ? client._seqTracker.getSyncState(ns)
2732
+ : { ack: seq, tail: seq, head: seq };
2733
+ this.mergeSeqTrackerPending(context, { [ns]: state });
1599
2734
  this.scheduleSeqTrackerFlush();
1600
2735
  }
2736
+ async commitSeqTrackerState(ns) {
2737
+ const client = this.runtime.client;
2738
+ const context = this.currentSeqTrackerContext();
2739
+ if (!context || !ns)
2740
+ return;
2741
+ const current = client._seqTracker.getContiguousSeq(ns);
2742
+ const state = typeof client._seqTracker.getSyncState === 'function'
2743
+ ? client._seqTracker.getSyncState(ns)
2744
+ : { ack: current, tail: current, head: current };
2745
+ if (![state.ack, state.tail, state.head].some((value) => Number(value) > 0))
2746
+ return;
2747
+ const [aid, deviceId, slotId] = JSON.parse(context);
2748
+ await this.writeSeqTrackerState(aid, deviceId, slotId, { [ns]: state }, true);
2749
+ this.dropSeqTrackerPending(ns);
2750
+ client._clientLog.debug(`seq_tracker atomic commit: ns=${ns} A/T/H=${JSON.stringify(state)} context=${context}`);
2751
+ }
1601
2752
  async flushSeqTrackerPending() {
1602
2753
  const client = this.runtime.client;
1603
2754
  this.cancelSeqTrackerFlushTimer();
@@ -1643,10 +2794,12 @@ export class MessageDeliveryEngine {
1643
2794
  void this.flushSeqTrackerPending();
1644
2795
  }
1645
2796
  client._seqTrackerPendingPersistContext = context;
1646
- for (const [ns, seq] of Object.entries(state)) {
1647
- const seqNum = Number(seq);
1648
- if (ns && Number.isFinite(seqNum) && seqNum > 0) {
1649
- pending.set(ns, seqNum);
2797
+ for (const [ns, raw] of Object.entries(state)) {
2798
+ const ack = Number(raw.ack);
2799
+ const tail = Number(raw.tail);
2800
+ const head = Number(raw.head);
2801
+ if (ns && [ack, tail, head].every(Number.isFinite) && Math.max(ack, tail, head) > 0) {
2802
+ pending.set(ns, { ack, tail, head });
1650
2803
  }
1651
2804
  }
1652
2805
  }
@@ -1669,14 +2822,19 @@ export class MessageDeliveryEngine {
1669
2822
  client._seqTrackerFlushTimer = null;
1670
2823
  }
1671
2824
  }
1672
- async writeSeqTrackerState(aid, deviceId, slotId, state) {
2825
+ async writeSeqTrackerState(aid, deviceId, slotId, state, throwOnError = false) {
1673
2826
  const client = this.runtime.client;
1674
2827
  if (!aid || Object.keys(state).length === 0)
1675
2828
  return;
1676
2829
  try {
2830
+ const saveSync = client._tokenStore.saveSyncStates?.bind(client._tokenStore);
2831
+ if (typeof saveSync === 'function') {
2832
+ await saveSync(aid, deviceId, slotId, state);
2833
+ return;
2834
+ }
1677
2835
  const saveFn = client._tokenStore.saveSeq?.bind(client._tokenStore);
1678
2836
  if (typeof saveFn === 'function') {
1679
- await Promise.all(Object.entries(state).map(([ns, seq]) => saveFn(aid, deviceId, slotId, ns, seq)));
2837
+ await Promise.all(Object.entries(state).map(([ns, triplet]) => saveFn(aid, deviceId, slotId, ns, triplet.ack)));
1680
2838
  return;
1681
2839
  }
1682
2840
  if (typeof client._tokenStore.updateInstanceState === 'function') {
@@ -1690,13 +2848,17 @@ export class MessageDeliveryEngine {
1690
2848
  }
1691
2849
  }
1692
2850
  catch (exc) {
2851
+ const error = formatDeliveryError(exc);
2852
+ client._clientLog.warn(`save SeqTracker state failed: ${error}`);
1693
2853
  client._dispatcher.publish('seq_tracker.persist_error', {
1694
2854
  phase: 'save',
1695
2855
  aid,
1696
2856
  device_id: deviceId,
1697
2857
  slot_id: slotId,
1698
- error: String(exc),
2858
+ error: String(error),
1699
2859
  }).catch(() => { });
2860
+ if (throwOnError)
2861
+ throw exc;
1700
2862
  }
1701
2863
  }
1702
2864
  persistRepairedSeq(ns) {
@@ -1743,16 +2905,27 @@ export class MessageDeliveryEngine {
1743
2905
  clampAckSeq(method, field, ns, seq) {
1744
2906
  const client = this.runtime.client;
1745
2907
  const original = seq;
1746
- let next = Number(seq);
1747
- if (!Number.isFinite(next))
2908
+ let next = seq;
2909
+ if (typeof next !== 'number' || !Number.isSafeInteger(next))
1748
2910
  return 0;
1749
2911
  if (next < 0)
1750
2912
  next = 0;
1751
2913
  if (ns) {
1752
- const maxSeen = client._seqTracker.getMaxSeenSeq(ns);
1753
- if (maxSeen > 0 && next > maxSeen) {
1754
- client._clientLog.warn(`ack clamp: method=${method} ${field}=${original} > max_seen=${maxSeen}, clamp`);
1755
- next = maxSeen;
2914
+ const committedAck = client._seqTracker.getContiguousSeq(ns);
2915
+ if (next > committedAck) {
2916
+ client._clientLog.warn(`ack clamp: method=${method} ${field}=${original} > committed_ack=${committedAck}, clamp`);
2917
+ next = committedAck;
2918
+ }
2919
+ if ((method === 'message.v2.ack' || method === 'message.ack')) {
2920
+ const pending = this.pendingP2PInlineAcks?.get(ns);
2921
+ if (pending && !pending.committed && next >= pending.seq)
2922
+ next = pending.seq - 1;
2923
+ }
2924
+ else if (method === 'group.v2.ack' || method === 'group.ack_messages') {
2925
+ const canonicalNs = this.resolvePendingGroupInlineAckNamespace(ns);
2926
+ const pending = this.pendingGroupInlineAcks?.get(canonicalNs);
2927
+ if (pending && !pending.committed && next >= pending.seq)
2928
+ next = pending.seq - 1;
1756
2929
  }
1757
2930
  }
1758
2931
  return next;
@@ -1761,7 +2934,8 @@ export class MessageDeliveryEngine {
1761
2934
  const client = this.runtime.client;
1762
2935
  const clampField = (field, ns) => {
1763
2936
  const raw = params[field];
1764
- if (typeof raw !== 'number' || !Number.isFinite(raw))
2937
+ // 保留非法类型/数值,让 V2 ACK 严格入口失败关闭,不能先 clamp 成合法游标。
2938
+ if (typeof raw !== 'number' || !Number.isSafeInteger(raw))
1765
2939
  return params;
1766
2940
  const next = this.clampAckSeq(method, field, ns, raw);
1767
2941
  return next === raw ? params : { ...params, [field]: next };
@@ -1775,7 +2949,7 @@ export class MessageDeliveryEngine {
1775
2949
  return clampField('seq', ns);
1776
2950
  }
1777
2951
  if (method === 'group.v2.ack' || method === 'group.ack_messages' || method === 'group.ack_events') {
1778
- const groupId = normalizeGroupId(String(params.group_id ?? '')) || String(params.group_id ?? '').trim();
2952
+ const groupId = String(params.group_id ?? params.group_aid ?? '').trim();
1779
2953
  const ns = groupId ? `group:${groupId}` : '';
1780
2954
  if (method === 'group.v2.ack')
1781
2955
  return clampField('up_to_seq', ns);
@@ -1786,7 +2960,7 @@ export class MessageDeliveryEngine {
1786
2960
  }
1787
2961
  return params;
1788
2962
  }
1789
- async drainOrderedMessages(ns, beforeSeq) {
2963
+ async drainOrderedMessages(ns, beforeSeq, pullResponse = false) {
1790
2964
  const client = this.runtime.client;
1791
2965
  const queue = client._pendingOrderedMsgs.get(ns);
1792
2966
  if (!queue || queue.size === 0)
@@ -1801,7 +2975,7 @@ export class MessageDeliveryEngine {
1801
2975
  queue.delete(seq);
1802
2976
  if (!item || client._pushedSeqs.get(ns)?.has(seq))
1803
2977
  continue;
1804
- await this.publishOrderedQueueItem(ns, item.event, seq, item.payload);
2978
+ await this.publishOrderedQueueItem(ns, item.event, seq, item.payload, pullResponse);
1805
2979
  this.markPublishedSeq(ns, seq);
1806
2980
  delivered = true;
1807
2981
  }
@@ -1830,7 +3004,7 @@ export class MessageDeliveryEngine {
1830
3004
  this.enqueueOrderedMessage(ns, event, seqNum, payload);
1831
3005
  return false;
1832
3006
  }
1833
- await this.drainOrderedMessages(ns, seqNum);
3007
+ await this.drainOrderedMessages(ns, seqNum, true);
1834
3008
  if (client._pushedSeqs.get(ns)?.has(seqNum))
1835
3009
  return false;
1836
3010
  const queue = client._pendingOrderedMsgs.get(ns);
@@ -1849,9 +3023,9 @@ export class MessageDeliveryEngine {
1849
3023
  const seqNum = Number(seq);
1850
3024
  if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0 || !ns) {
1851
3025
  if (event === 'message.recalled') {
1852
- return this.publishMessageRecallTombstone(seq, payload);
3026
+ return await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seq, payload));
1853
3027
  }
1854
- await client._publishAppEvent(event, payload);
3028
+ await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload));
1855
3029
  return true;
1856
3030
  }
1857
3031
  const queue = client._pendingOrderedMsgs.get(ns);
@@ -1866,11 +3040,11 @@ export class MessageDeliveryEngine {
1866
3040
  if (queue && queue.size === 0)
1867
3041
  client._pendingOrderedMsgs.delete(ns);
1868
3042
  if (event === 'message.recalled') {
1869
- const published = await this.publishMessageRecallTombstone(seqNum, payload);
3043
+ const published = await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seqNum, payload));
1870
3044
  this.markPublishedSeq(ns, seqNum);
1871
3045
  return published;
1872
3046
  }
1873
- await client._publishAppEvent(event, payload);
3047
+ await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload));
1874
3048
  this.markPublishedSeq(ns, seqNum);
1875
3049
  return true;
1876
3050
  }