@agentunion/fastaun-browser 0.5.4 → 0.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/CHANGELOG.md +177 -67
  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 +177 -67
  6. package/_packed_docs/INDEX.md +124 -65
  7. package/_packed_docs/KITE_DOCS_GUIDE.md +65 -30
  8. package/_packed_docs/agent.md/SCHEMA.md +83 -58
  9. package/_packed_docs/agent.md/examples/codeagent-claudecode.md +1 -1
  10. package/_packed_docs/agent.md/examples/openclaw-lobster.md +1 -1
  11. package/_packed_docs/agent.md/examples/signed-openclaw-lobster.md +1 -1
  12. 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
  13. 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 +234 -0
  14. 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 +1095 -0
  15. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +1199 -0
  16. package/_packed_docs/cli/CLI/346/211/213/345/206/214.md +6 -4
  17. package/_packed_docs/group-message-rpc-alignment-gaps.md +741 -0
  18. package/_packed_docs/message-online-push-alignment.md +572 -0
  19. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +58 -20
  20. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +219 -247
  21. package/_packed_docs/protocol/12-Stream-/345/255/220/345/215/217/350/256/256.md +14 -14
  22. package/_packed_docs/protocol/13-Agent/350/241/214/344/270/272/350/247/204/350/214/203.md +3 -3
  23. 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
  24. package/_packed_docs/protocol/README.md +1 -0
  25. package/_packed_docs/protocol/aun-docs-guide.md +7 -4
  26. package/_packed_docs/protocol/index.md +25 -19
  27. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +63 -18
  28. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +22 -0
  29. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +93 -84
  30. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +6 -2
  31. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +73 -34
  32. package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +3 -3
  33. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +21 -9
  34. package/_packed_docs/sdk/09-group-rpc-manual.md +319 -285
  35. package/_packed_docs/sdk/09-message-rpc-manual.md +144 -103
  36. package/_packed_docs/sdk/09-payload-reference.md +1 -1
  37. package/_packed_docs/sdk/09-storage-rpc-manual.md +14 -3
  38. package/_packed_docs/sdk/09-stream-rpc-manual.md +8 -8
  39. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +34 -23
  40. package/_packed_docs/sdk/CHANGELOG-0.5.6.md +73 -0
  41. package/_packed_docs/sdk/INDEX.md +45 -37
  42. package/_packed_docs/sdk/README.md +11 -9
  43. package/_packed_docs//345/217/221/345/270/203/346/212/245/345/221/212-0.5.6.md +260 -0
  44. 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
  45. 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
  46. package/dist/agent-md-schema.d.ts +14 -0
  47. package/dist/agent-md-schema.d.ts.map +1 -0
  48. package/dist/agent-md-schema.js +103 -0
  49. package/dist/agent-md-schema.js.map +1 -0
  50. package/dist/agent-md.d.ts +11 -5
  51. package/dist/agent-md.d.ts.map +1 -1
  52. package/dist/agent-md.js +295 -53
  53. package/dist/agent-md.js.map +1 -1
  54. package/dist/aid-store.d.ts +7 -7
  55. package/dist/aid-store.d.ts.map +1 -1
  56. package/dist/aid-store.js +46 -18
  57. package/dist/aid-store.js.map +1 -1
  58. package/dist/aid.d.ts +2 -0
  59. package/dist/aid.d.ts.map +1 -1
  60. package/dist/aid.js +29 -7
  61. package/dist/aid.js.map +1 -1
  62. package/dist/auth.d.ts.map +1 -1
  63. package/dist/auth.js +31 -11
  64. package/dist/auth.js.map +1 -1
  65. package/dist/bundle.js +29920 -19719
  66. package/dist/cert-utils.d.ts +1 -0
  67. package/dist/cert-utils.d.ts.map +1 -1
  68. package/dist/cert-utils.js +36 -16
  69. package/dist/cert-utils.js.map +1 -1
  70. package/dist/client/delivery.d.ts +106 -11
  71. package/dist/client/delivery.d.ts.map +1 -1
  72. package/dist/client/delivery.js +1925 -374
  73. package/dist/client/delivery.js.map +1 -1
  74. package/dist/client/group-state.d.ts.map +1 -1
  75. package/dist/client/group-state.js +27 -24
  76. package/dist/client/group-state.js.map +1 -1
  77. package/dist/client/lifecycle.d.ts +15 -1
  78. package/dist/client/lifecycle.d.ts.map +1 -1
  79. package/dist/client/lifecycle.js +477 -132
  80. package/dist/client/lifecycle.js.map +1 -1
  81. package/dist/client/mention-mode.d.ts +7 -0
  82. package/dist/client/mention-mode.d.ts.map +1 -0
  83. package/dist/client/mention-mode.js +184 -0
  84. package/dist/client/mention-mode.js.map +1 -0
  85. package/dist/client/peers.d.ts +1 -1
  86. package/dist/client/peers.d.ts.map +1 -1
  87. package/dist/client/peers.js +26 -3
  88. package/dist/client/peers.js.map +1 -1
  89. package/dist/client/rpc-pipeline.d.ts +34 -2
  90. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  91. package/dist/client/rpc-pipeline.js +503 -99
  92. package/dist/client/rpc-pipeline.js.map +1 -1
  93. package/dist/client/runtime.d.ts +1 -3
  94. package/dist/client/runtime.d.ts.map +1 -1
  95. package/dist/client/runtime.js +4 -7
  96. package/dist/client/runtime.js.map +1 -1
  97. package/dist/client/v2-e2ee.d.ts +43 -2
  98. package/dist/client/v2-e2ee.d.ts.map +1 -1
  99. package/dist/client/v2-e2ee.js +931 -138
  100. package/dist/client/v2-e2ee.js.map +1 -1
  101. package/dist/client.d.ts +46 -15
  102. package/dist/client.d.ts.map +1 -1
  103. package/dist/client.js +785 -304
  104. package/dist/client.js.map +1 -1
  105. package/dist/errors.d.ts.map +1 -1
  106. package/dist/errors.js +4 -1
  107. package/dist/errors.js.map +1 -1
  108. package/dist/facades.d.ts +26 -1
  109. package/dist/facades.d.ts.map +1 -1
  110. package/dist/facades.js +142 -60
  111. package/dist/facades.js.map +1 -1
  112. package/dist/group-id.d.ts.map +1 -1
  113. package/dist/group-id.js +2 -17
  114. package/dist/group-id.js.map +1 -1
  115. package/dist/group-index.d.ts +6 -1
  116. package/dist/group-index.d.ts.map +1 -1
  117. package/dist/group-index.js +44 -25
  118. package/dist/group-index.js.map +1 -1
  119. package/dist/index.d.ts +5 -4
  120. package/dist/index.d.ts.map +1 -1
  121. package/dist/index.js +3 -2
  122. package/dist/index.js.map +1 -1
  123. package/dist/keystore/index.d.ts +10 -5
  124. package/dist/keystore/index.d.ts.map +1 -1
  125. package/dist/keystore/indexeddb-identity-store.d.ts +0 -12
  126. package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
  127. package/dist/keystore/indexeddb-identity-store.js +0 -60
  128. package/dist/keystore/indexeddb-identity-store.js.map +1 -1
  129. package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
  130. package/dist/keystore/indexeddb-shared.js +9 -5
  131. package/dist/keystore/indexeddb-shared.js.map +1 -1
  132. package/dist/keystore/indexeddb-token-store.d.ts +3 -1
  133. package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
  134. package/dist/keystore/indexeddb-token-store.js +47 -2
  135. package/dist/keystore/indexeddb-token-store.js.map +1 -1
  136. package/dist/register-flow.d.ts.map +1 -1
  137. package/dist/register-flow.js +28 -3
  138. package/dist/register-flow.js.map +1 -1
  139. package/dist/seq-tracker.d.ts +28 -8
  140. package/dist/seq-tracker.d.ts.map +1 -1
  141. package/dist/seq-tracker.js +224 -61
  142. package/dist/seq-tracker.js.map +1 -1
  143. package/dist/storage/vfs.d.ts +1 -0
  144. package/dist/storage/vfs.d.ts.map +1 -1
  145. package/dist/storage/vfs.js +26 -2
  146. package/dist/storage/vfs.js.map +1 -1
  147. package/dist/tools/cross-sdk-agent.js +399 -11
  148. package/dist/tools/cross-sdk-agent.js.map +1 -1
  149. package/dist/transport.d.ts +12 -0
  150. package/dist/transport.d.ts.map +1 -1
  151. package/dist/transport.js +226 -102
  152. package/dist/transport.js.map +1 -1
  153. package/dist/v2/session/session.d.ts.map +1 -1
  154. package/dist/v2/session/session.js +3 -4
  155. package/dist/v2/session/session.js.map +1 -1
  156. package/dist/v2/state/commitment.d.ts.map +1 -1
  157. package/dist/v2/state/commitment.js +1 -2
  158. package/dist/v2/state/commitment.js.map +1 -1
  159. package/dist/validators.d.ts.map +1 -1
  160. package/dist/validators.js +8 -14
  161. package/dist/validators.js.map +1 -1
  162. package/dist/version.d.ts +1 -1
  163. package/dist/version.js +1 -1
  164. package/package.json +11 -9
  165. package/dist/group-resources.d.ts +0 -98
  166. package/dist/group-resources.d.ts.map +0 -1
  167. package/dist/group-resources.js +0 -635
  168. package/dist/group-resources.js.map +0 -1
  169. package/dist/keystore/indexeddb.d.ts +0 -179
  170. package/dist/keystore/indexeddb.d.ts.map +0 -1
  171. package/dist/keystore/indexeddb.js +0 -2031
  172. package/dist/keystore/indexeddb.js.map +0 -1
  173. package/dist/namespaces/auth.d.ts +0 -98
  174. package/dist/namespaces/auth.d.ts.map +0 -1
  175. package/dist/namespaces/auth.js +0 -992
  176. package/dist/namespaces/auth.js.map +0 -1
  177. package/dist/namespaces/custody.d.ts +0 -51
  178. package/dist/namespaces/custody.d.ts.map +0 -1
  179. package/dist/namespaces/custody.js +0 -302
  180. package/dist/namespaces/custody.js.map +0 -1
  181. package/dist/namespaces/meta.d.ts +0 -109
  182. package/dist/namespaces/meta.d.ts.map +0 -1
  183. package/dist/namespaces/meta.js +0 -549
  184. 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,34 @@ 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
+ }
60
+ function nonNegativeSafeSequenceHint(value) {
61
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
62
+ return null;
63
+ return value;
64
+ }
65
+ function deduplicatePlainForwardPageMessages(value, afterSeq) {
66
+ if (!Array.isArray(value))
67
+ return [];
68
+ const bySeq = new Map();
69
+ for (const raw of value) {
70
+ if (!isJsonObject(raw))
71
+ continue;
72
+ const seq = positiveSafeSequenceHint(raw.seq);
73
+ if (seq <= afterSeq || bySeq.has(seq))
74
+ continue;
75
+ bySeq.set(seq, { ...raw, seq });
76
+ }
77
+ return [...bySeq.entries()].sort(([left], [right]) => left - right).map(([, message]) => message);
78
+ }
50
79
  function p2pAppEventFromPlainPullMessage(message) {
51
80
  const payload = message.payload;
52
81
  const payloadType = isJsonObject(payload)
@@ -65,9 +94,146 @@ function p2pAppEventFromPlainPullMessage(message) {
65
94
  }
66
95
  export class MessageDeliveryEngine {
67
96
  runtime;
97
+ realtimeTailResults = null;
98
+ realtimeSyncing = null;
99
+ pendingP2pPullUpper = null;
100
+ pendingGroupPullUpper = null;
101
+ realtimeAcking = null;
102
+ pendingP2PInlineAcks = null;
103
+ pendingGroupInlineAcks = null;
104
+ pendingGroupInlineAckNamespaces = null;
105
+ // reset 后旧 inline Promise 只能自行结束,不能触碰新身份上下文的状态。
106
+ inlineGeneration = 0;
107
+ // 同一实例的 tracker 写入必须串行,避免旧的延迟 flush 覆盖新的 Forward 提交。
108
+ seqTrackerWriteChain = Promise.resolve();
68
109
  constructor(runtime) {
69
110
  this.runtime = runtime;
70
111
  }
112
+ resetInlineAckState() {
113
+ this.inlineGeneration += 1;
114
+ this.realtimeSyncing = null;
115
+ this.pendingP2pPullUpper = null;
116
+ this.pendingGroupPullUpper = null;
117
+ this.realtimeTailResults = null;
118
+ this.realtimeAcking = null;
119
+ this.pendingP2PInlineAcks = null;
120
+ this.pendingGroupInlineAcks = null;
121
+ this.pendingGroupInlineAckNamespaces = null;
122
+ }
123
+ isInlineGenerationCurrent(generation) {
124
+ return generation === this.inlineGeneration;
125
+ }
126
+ captureInlineGeneration() {
127
+ return this.inlineGeneration;
128
+ }
129
+ beginRealtimeSync(ns) {
130
+ if (!ns || this.realtimeSyncing?.has(ns))
131
+ return false;
132
+ const syncing = this.realtimeSyncing ?? new Set();
133
+ this.realtimeSyncing = syncing;
134
+ syncing.add(ns);
135
+ return true;
136
+ }
137
+ endRealtimeSync(ns) {
138
+ this.realtimeSyncing?.delete(ns);
139
+ }
140
+ hasPendingPull(ns) {
141
+ const pending = ns.startsWith('p2p:') ? this.pendingP2pPullUpper : this.pendingGroupPullUpper;
142
+ return (pending?.get(ns) ?? 0) > 0;
143
+ }
144
+ onPullGateIdle(ns) {
145
+ if (!ns)
146
+ return;
147
+ this.schedulePendingPullIfNeeded(ns, 'pull-gate-idle');
148
+ }
149
+ recordPendingPull(ns, seq) {
150
+ if (!ns || !Number.isSafeInteger(seq) || seq <= 0)
151
+ return;
152
+ const pending = ns.startsWith('p2p:')
153
+ ? (this.pendingP2pPullUpper ?? new Map())
154
+ : (this.pendingGroupPullUpper ?? new Map());
155
+ if (ns.startsWith('p2p:'))
156
+ this.pendingP2pPullUpper = pending;
157
+ else
158
+ this.pendingGroupPullUpper = pending;
159
+ pending.set(ns, Math.max(pending.get(ns) ?? 0, seq));
160
+ }
161
+ consumePendingPull(ns, throughSeq) {
162
+ const pending = ns.startsWith('p2p:') ? this.pendingP2pPullUpper : this.pendingGroupPullUpper;
163
+ if ((pending?.get(ns) ?? 0) <= throughSeq)
164
+ pending?.delete(ns);
165
+ }
166
+ schedulePendingPullIfNeeded(ns, reason) {
167
+ const pending = ns.startsWith('p2p:') ? this.pendingP2pPullUpper : this.pendingGroupPullUpper;
168
+ if (!pending)
169
+ return false;
170
+ const upper = pending.get(ns) ?? 0;
171
+ if (upper <= 0) {
172
+ pending.delete(ns);
173
+ return false;
174
+ }
175
+ const client = this.runtime.client;
176
+ const contiguous = client._seqTracker.getContiguousSeq(ns);
177
+ if (upper <= contiguous) {
178
+ pending.delete(ns);
179
+ client._clientLog?.debug(`pending pull upper already covered: ns=${ns}, upper_seq=${upper}, contiguous=${contiguous}, reason=${reason}`);
180
+ return false;
181
+ }
182
+ if (client.state !== ConnectionState.READY || client._closing || this.realtimeSyncing?.has(ns)
183
+ || client._rpcPipeline?.hasPullActivity?.(ns, false))
184
+ return false;
185
+ pending.delete(ns);
186
+ client._clientLog?.debug(`pending push follow-up pull scheduled: ns=${ns}, upper_seq=${upper}, reason=${reason}`);
187
+ if (ns.startsWith('p2p:')) {
188
+ client._safeAsync(this.onV2PushNotification({ seq: upper }));
189
+ }
190
+ else {
191
+ client._safeAsync(this.onRawGroupV2MessageCreated({ group_id: ns.slice('group:'.length), seq: upper }));
192
+ }
193
+ return true;
194
+ }
195
+ beginRealtimeAck(ns, seq) {
196
+ if (!ns || !Number.isSafeInteger(seq) || seq <= 0)
197
+ return false;
198
+ const current = this.realtimeAcking?.get(ns);
199
+ if (current !== undefined) {
200
+ if (seq > current)
201
+ this.realtimeAcking?.set(ns, seq);
202
+ return false;
203
+ }
204
+ const acking = this.realtimeAcking ?? new Map();
205
+ this.realtimeAcking = acking;
206
+ acking.set(ns, seq);
207
+ return true;
208
+ }
209
+ finishRealtimeAck(ns, completedSeq) {
210
+ const latest = this.realtimeAcking?.get(ns) ?? 0;
211
+ this.realtimeAcking?.delete(ns);
212
+ return latest > completedSeq ? latest : 0;
213
+ }
214
+ pendingInlineAckNamespaceCount() {
215
+ return (this.pendingP2PInlineAcks?.size ?? 0) + (this.pendingGroupInlineAcks?.size ?? 0);
216
+ }
217
+ inlinePostAckRow(row) {
218
+ const result = {};
219
+ for (const key of ['envelope_json', 'spk_id', 'group_id', 'message_id']) {
220
+ const value = row[key];
221
+ if (typeof value === 'string' && value)
222
+ result[key] = value;
223
+ }
224
+ if (typeof row.seq === 'number' && Number.isSafeInteger(row.seq) && row.seq > 0)
225
+ result.seq = row.seq;
226
+ return result;
227
+ }
228
+ markRealtimeTailCompleted(result) {
229
+ const completed = this.realtimeTailResults ?? new WeakSet();
230
+ this.realtimeTailResults = completed;
231
+ completed.add(result);
232
+ Object.defineProperty(result, '_realtime_tail_completed', { value: true, enumerable: false });
233
+ }
234
+ isRealtimeTailCompleted(result) {
235
+ return this.realtimeTailResults?.has(result) ?? false;
236
+ }
71
237
  prunePushedSeqs(ns) {
72
238
  const client = this.runtime.client;
73
239
  const pushed = client._pushedSeqs.get(ns);
@@ -106,18 +272,510 @@ export class MessageDeliveryEngine {
106
272
  client._clientLog.warn(`${label} failed: group=${groupId}`, e);
107
273
  });
108
274
  }
109
- fireGroupV2Ack(groupId, ns, seq, label) {
275
+ fireGroupV2Ack(groupId, ns, seq, label, generation = null) {
110
276
  const client = this.runtime.client;
111
277
  const ackSeq = this.clampAckSeq('group.v2.ack', 'up_to_seq', ns, seq);
112
- if (ackSeq <= 0)
278
+ if (ackSeq <= 0 || !this.beginRealtimeAck(ns, ackSeq))
113
279
  return;
114
- void client._callRawV2Rpc('group.v2.ack', {
115
- group_id: groupId,
116
- up_to_seq: ackSeq,
117
- _rpc_background: true,
280
+ let task;
281
+ try {
282
+ task = Promise.resolve(client._callRawV2Rpc('group.v2.ack', {
283
+ group_id: groupId,
284
+ up_to_seq: ackSeq,
285
+ _rpc_background: true,
286
+ }));
287
+ }
288
+ catch (exc) {
289
+ this.finishRealtimeAck(ns, ackSeq);
290
+ client._clientLog.debug(`${label} failed: group=${groupId} seq=${ackSeq} err=${formatDeliveryError(exc)}`);
291
+ return;
292
+ }
293
+ void task.then((result) => {
294
+ if (generation !== null && this.isInlineGenerationCurrent(generation)) {
295
+ this.completePendingGroupInlineAckUpTo(ns, this.resolveActualAckSeq(result, ackSeq), generation);
296
+ }
118
297
  }).catch((e) => {
119
298
  client._clientLog.debug(`${label} failed: group=${groupId} seq=${ackSeq} err=${String(e)}`);
299
+ }).finally(() => {
300
+ if (generation !== null && !this.isInlineGenerationCurrent(generation))
301
+ return;
302
+ const nextAckSeq = this.finishRealtimeAck(ns, ackSeq);
303
+ if (nextAckSeq > 0)
304
+ this.fireGroupV2Ack(groupId, ns, nextAckSeq, label, generation);
305
+ });
306
+ }
307
+ fireP2PV2Ack(seq, label, generation = null) {
308
+ const client = this.runtime.client;
309
+ const ns = client._aid ? `p2p:${client._aid}` : '';
310
+ if (!Number.isSafeInteger(seq) || seq <= 0 || typeof client._ackV2 !== 'function'
311
+ || !this.beginRealtimeAck(ns, seq))
312
+ return;
313
+ try {
314
+ const ackTask = generation === null ? client._ackV2(seq) : client._ackV2(seq, generation);
315
+ void Promise.resolve(ackTask).catch((exc) => {
316
+ client._clientLog.debug(`${label} failed: seq=${seq} err=${formatDeliveryError(exc)}`);
317
+ }).finally(() => {
318
+ if (generation !== null && !this.isInlineGenerationCurrent(generation))
319
+ return;
320
+ const nextAckSeq = this.finishRealtimeAck(ns, seq);
321
+ if (nextAckSeq > 0)
322
+ this.fireP2PV2Ack(nextAckSeq, label, generation);
323
+ });
324
+ }
325
+ catch (exc) {
326
+ this.finishRealtimeAck(ns, seq);
327
+ client._clientLog.debug(`${label} failed: seq=${seq} err=${formatDeliveryError(exc)}`);
328
+ }
329
+ }
330
+ retryPendingP2PInlineAck(ns, seq, label, ackThrough = seq) {
331
+ const pending = this.pendingP2PInlineAcks?.get(ns);
332
+ if (!pending || pending.seq !== seq)
333
+ return false;
334
+ if (pending.acking || !pending.committed)
335
+ return true;
336
+ if (!Number.isSafeInteger(ackThrough) || ackThrough < seq)
337
+ ackThrough = seq;
338
+ const client = this.runtime.client;
339
+ if (typeof client._ackV2 !== 'function')
340
+ return true;
341
+ pending.acking = true;
342
+ const generation = pending.generation;
343
+ let task;
344
+ try {
345
+ task = Promise.resolve(client._ackV2(ackThrough, generation));
346
+ }
347
+ catch (exc) {
348
+ pending.acking = false;
349
+ client._clientLog.debug(`${label} failed: seq=${seq} err=${formatDeliveryError(exc)}`);
350
+ return true;
351
+ }
352
+ void task.catch((exc) => {
353
+ client._clientLog.debug(`${label} failed: seq=${seq} err=${formatDeliveryError(exc)}`);
354
+ }).finally(() => {
355
+ const current = this.pendingP2PInlineAcks?.get(ns);
356
+ if (this.isInlineGenerationCurrent(generation) && current === pending)
357
+ current.acking = false;
358
+ });
359
+ return true;
360
+ }
361
+ retryPendingGroupInlineAck(ns, seq, label, ackThrough = seq) {
362
+ const pending = this.pendingGroupInlineAcks?.get(ns);
363
+ if (!pending || pending.seq !== seq)
364
+ return false;
365
+ if (pending.acking || !pending.committed)
366
+ return true;
367
+ if (!Number.isSafeInteger(ackThrough) || ackThrough < seq)
368
+ ackThrough = seq;
369
+ const client = this.runtime.client;
370
+ if (typeof client._ackGroupV2 !== 'function')
371
+ return true;
372
+ pending.acking = true;
373
+ const generation = pending.generation;
374
+ let task;
375
+ try {
376
+ task = Promise.resolve(client._ackGroupV2(pending.groupId, ackThrough, pending.groupAid || undefined, generation));
377
+ }
378
+ catch (exc) {
379
+ pending.acking = false;
380
+ client._clientLog.debug(`${label} failed: group=${pending.groupId} seq=${seq} err=${formatDeliveryError(exc)}`);
381
+ return true;
382
+ }
383
+ void task.catch((exc) => {
384
+ client._clientLog.debug(`${label} failed: group=${pending.groupId} seq=${seq} err=${formatDeliveryError(exc)}`);
385
+ }).finally(() => {
386
+ const current = this.pendingGroupInlineAcks?.get(ns);
387
+ if (this.isInlineGenerationCurrent(generation) && current === pending)
388
+ current.acking = false;
389
+ });
390
+ return true;
391
+ }
392
+ reservePendingP2PInlineAck(ns, seq, row, generation = this.inlineGeneration) {
393
+ if (!this.isInlineGenerationCurrent(generation)
394
+ || !ns || !Number.isSafeInteger(seq) || seq <= 0 || this.pendingP2PInlineAcks?.has(ns)
395
+ || this.pendingInlineAckNamespaceCount() >= MAX_INLINE_PENDING_ACK_NAMESPACES)
396
+ return false;
397
+ const pendingMap = this.pendingP2PInlineAcks ?? new Map();
398
+ this.pendingP2PInlineAcks = pendingMap;
399
+ pendingMap.set(ns, {
400
+ seq,
401
+ row: this.inlinePostAckRow(row),
402
+ generation,
403
+ committed: false,
404
+ observedAckSeq: 0,
405
+ acking: false,
406
+ });
407
+ return true;
408
+ }
409
+ dropPendingP2PInlineAck(ns, seq, generation = this.inlineGeneration) {
410
+ const pending = this.pendingP2PInlineAcks?.get(ns);
411
+ if (this.isInlineGenerationCurrent(generation) && pending?.seq === seq && pending.generation === generation) {
412
+ this.pendingP2PInlineAcks?.delete(ns);
413
+ }
414
+ }
415
+ reservePendingGroupInlineAck(ns, seq, row, groupAid, groupId, generation = this.inlineGeneration) {
416
+ if (!this.isInlineGenerationCurrent(generation)
417
+ || !ns || !groupId || !Number.isSafeInteger(seq) || seq <= 0
418
+ || this.pendingGroupInlineAcks?.has(ns)
419
+ || this.pendingGroupInlineAckNamespaces?.has(ns)
420
+ || this.pendingInlineAckNamespaceCount() >= MAX_INLINE_PENDING_ACK_NAMESPACES)
421
+ return false;
422
+ const normalizedGroupAid = typeof groupAid === 'string' ? groupAid.trim() : '';
423
+ const aliasNs = normalizedGroupAid ? `group:${normalizedGroupAid}` : '';
424
+ if (aliasNs && aliasNs !== ns
425
+ && (this.pendingGroupInlineAcks?.has(aliasNs) || this.pendingGroupInlineAckNamespaces?.has(aliasNs)))
426
+ return false;
427
+ const pendingMap = this.pendingGroupInlineAcks ?? new Map();
428
+ this.pendingGroupInlineAcks = pendingMap;
429
+ pendingMap.set(ns, {
430
+ seq,
431
+ row: this.inlinePostAckRow(row),
432
+ generation,
433
+ groupId,
434
+ groupAid: normalizedGroupAid,
435
+ aliasNs: aliasNs !== ns ? aliasNs : '',
436
+ committed: false,
437
+ observedAckSeq: 0,
438
+ acking: false,
120
439
  });
440
+ if (aliasNs && aliasNs !== ns) {
441
+ const aliasMap = this.pendingGroupInlineAckNamespaces ?? new Map();
442
+ this.pendingGroupInlineAckNamespaces = aliasMap;
443
+ aliasMap.set(aliasNs, ns);
444
+ }
445
+ return true;
446
+ }
447
+ removePendingGroupInlineAck(ns, pending) {
448
+ if (this.pendingGroupInlineAcks?.get(ns) !== pending)
449
+ return false;
450
+ this.pendingGroupInlineAcks.delete(ns);
451
+ if (pending.aliasNs && this.pendingGroupInlineAckNamespaces?.get(pending.aliasNs) === ns) {
452
+ this.pendingGroupInlineAckNamespaces.delete(pending.aliasNs);
453
+ }
454
+ return true;
455
+ }
456
+ dropPendingGroupInlineAck(ns, seq, generation = this.inlineGeneration) {
457
+ const pending = this.pendingGroupInlineAcks?.get(ns);
458
+ if (this.isInlineGenerationCurrent(generation) && pending?.seq === seq && pending.generation === generation) {
459
+ this.removePendingGroupInlineAck(ns, pending);
460
+ }
461
+ }
462
+ resolvePendingGroupInlineAckNamespace(ns) {
463
+ const pendingMap = this.pendingGroupInlineAcks;
464
+ if (!ns || !pendingMap || pendingMap.size === 0 || pendingMap.has(ns))
465
+ return ns;
466
+ const canonicalNs = this.pendingGroupInlineAckNamespaces?.get(ns);
467
+ return canonicalNs && pendingMap.has(canonicalNs) ? canonicalNs : ns;
468
+ }
469
+ pendingP2PInlineAckGeneration(ns) {
470
+ const pendingMap = this.pendingP2PInlineAcks;
471
+ if (!ns || !pendingMap || pendingMap.size === 0)
472
+ return null;
473
+ const pending = pendingMap.get(ns);
474
+ return pending && this.isInlineGenerationCurrent(pending.generation) ? pending.generation : null;
475
+ }
476
+ pendingGroupInlineAckGeneration(ns) {
477
+ const pendingMap = this.pendingGroupInlineAcks;
478
+ if (!ns || !pendingMap || pendingMap.size === 0)
479
+ return null;
480
+ const canonicalNs = pendingMap.has(ns) ? ns : this.pendingGroupInlineAckNamespaces?.get(ns);
481
+ if (!canonicalNs)
482
+ return null;
483
+ const pending = pendingMap.get(canonicalNs);
484
+ return pending && this.isInlineGenerationCurrent(pending.generation) ? pending.generation : null;
485
+ }
486
+ completePendingP2PInlineAckUpTo(ns, ackSeq, generation = this.inlineGeneration) {
487
+ const pendingMap = this.pendingP2PInlineAcks;
488
+ if (!this.isInlineGenerationCurrent(generation)
489
+ || !ns || !Number.isSafeInteger(ackSeq) || ackSeq <= 0 || !pendingMap || pendingMap.size === 0)
490
+ return;
491
+ const pending = pendingMap.get(ns);
492
+ if (!pending || pending.generation !== generation || ackSeq < pending.seq)
493
+ return;
494
+ if (!pending.committed) {
495
+ pending.observedAckSeq = Math.max(pending.observedAckSeq, ackSeq);
496
+ return;
497
+ }
498
+ pendingMap.delete(ns);
499
+ this.runPostInlineCommit(pending.row, pending.seq, 'P2P', generation);
500
+ }
501
+ completePendingGroupInlineAckUpTo(ns, ackSeq, generation = this.inlineGeneration) {
502
+ const pendingMap = this.pendingGroupInlineAcks;
503
+ if (!this.isInlineGenerationCurrent(generation)
504
+ || !ns || !Number.isSafeInteger(ackSeq) || ackSeq <= 0 || !pendingMap || pendingMap.size === 0)
505
+ return;
506
+ const canonicalNs = this.resolvePendingGroupInlineAckNamespace(ns);
507
+ const pending = pendingMap.get(canonicalNs);
508
+ if (!pending || pending.generation !== generation || ackSeq < pending.seq)
509
+ return;
510
+ if (!pending.committed) {
511
+ pending.observedAckSeq = Math.max(pending.observedAckSeq, ackSeq);
512
+ return;
513
+ }
514
+ this.removePendingGroupInlineAck(canonicalNs, pending);
515
+ this.runPostInlineCommit(pending.row, pending.seq, 'Group', generation);
516
+ }
517
+ runPostInlineCommit(row, seq, scope, generation) {
518
+ const client = this.runtime.client;
519
+ if (!this.isInlineGenerationCurrent(generation) || typeof client._postInlineV2CommitAndAck !== 'function')
520
+ return;
521
+ try {
522
+ void Promise.resolve(client._postInlineV2CommitAndAck(row)).catch((exc) => {
523
+ client._clientLog.debug(`inline ${scope} post-commit work failed: seq=${seq} err=${formatDeliveryError(exc)}`);
524
+ });
525
+ }
526
+ catch (exc) {
527
+ client._clientLog.debug(`inline ${scope} post-commit work failed: seq=${seq} err=${formatDeliveryError(exc)}`);
528
+ }
529
+ }
530
+ markPendingP2PInlineAckCommitted(ns, seq, generation = this.inlineGeneration) {
531
+ const pending = this.pendingP2PInlineAcks?.get(ns);
532
+ if (!this.isInlineGenerationCurrent(generation) || !pending || pending.seq !== seq || pending.generation !== generation)
533
+ return false;
534
+ pending.committed = true;
535
+ if (pending.observedAckSeq < seq)
536
+ return true;
537
+ this.pendingP2PInlineAcks?.delete(ns);
538
+ this.runPostInlineCommit(pending.row, pending.seq, 'P2P', generation);
539
+ return true;
540
+ }
541
+ markPendingGroupInlineAckCommitted(ns, seq, generation = this.inlineGeneration) {
542
+ const pending = this.pendingGroupInlineAcks?.get(ns);
543
+ if (!this.isInlineGenerationCurrent(generation) || !pending || pending.seq !== seq || pending.generation !== generation)
544
+ return false;
545
+ pending.committed = true;
546
+ if (pending.observedAckSeq < seq)
547
+ return true;
548
+ this.removePendingGroupInlineAck(ns, pending);
549
+ this.runPostInlineCommit(pending.row, pending.seq, 'Group', generation);
550
+ return true;
551
+ }
552
+ resolveActualAckSeq(result, requestedSeq) {
553
+ if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
554
+ return 0;
555
+ if (!isJsonObject(result))
556
+ return requestedSeq;
557
+ const row = result;
558
+ let value;
559
+ if (Object.prototype.hasOwnProperty.call(row, 'effective_ack_seq')) {
560
+ value = row.effective_ack_seq;
561
+ }
562
+ else if (Object.prototype.hasOwnProperty.call(row, 'ack_seq')) {
563
+ value = row.ack_seq;
564
+ }
565
+ else if (Object.prototype.hasOwnProperty.call(row, 'cursor')) {
566
+ const cursor = row.cursor;
567
+ if (isJsonObject(cursor)) {
568
+ const cursorRow = cursor;
569
+ if (!Object.prototype.hasOwnProperty.call(cursorRow, 'current_seq'))
570
+ return 0;
571
+ value = cursorRow.current_seq;
572
+ }
573
+ else {
574
+ value = cursor;
575
+ }
576
+ }
577
+ else {
578
+ return requestedSeq;
579
+ }
580
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
581
+ }
582
+ resolveP2PPullAckSeq(result, requestedSeq) {
583
+ if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
584
+ return 0;
585
+ if (isJsonObject(result)
586
+ && Object.prototype.hasOwnProperty.call(result, 'server_ack_seq')) {
587
+ const value = result.server_ack_seq;
588
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
589
+ }
590
+ return 0;
591
+ }
592
+ resolveGroupPullAckSeq(result, requestedSeq) {
593
+ if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
594
+ return 0;
595
+ if (!isJsonObject(result))
596
+ return 0;
597
+ const row = result;
598
+ if (!Object.prototype.hasOwnProperty.call(row, 'cursor'))
599
+ return 0;
600
+ const cursor = row.cursor;
601
+ if (!isJsonObject(cursor))
602
+ return 0;
603
+ const cursorRow = cursor;
604
+ if (!Object.prototype.hasOwnProperty.call(cursorRow, 'current_seq'))
605
+ return 0;
606
+ const value = cursorRow.current_seq;
607
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
608
+ }
609
+ resolveForwardAckSeq(result, requestedSeq) {
610
+ if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
611
+ return 0;
612
+ // 旧服务端成功响应可能不带累计水位字段;RPC 已成功,兼容按请求水位处理。
613
+ if (!isJsonObject(result))
614
+ return requestedSeq;
615
+ const row = result;
616
+ let value;
617
+ if (Object.prototype.hasOwnProperty.call(row, 'effective_ack_seq')) {
618
+ value = row.effective_ack_seq;
619
+ }
620
+ else if (Object.prototype.hasOwnProperty.call(row, 'ack_seq')) {
621
+ value = row.ack_seq;
622
+ }
623
+ else {
624
+ return requestedSeq;
625
+ }
626
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
627
+ }
628
+ forwardCoordinator() {
629
+ const coordinator = this.runtime.client._v2E2EE;
630
+ if (!coordinator
631
+ || typeof coordinator.pendingForwardCursor !== 'function'
632
+ || typeof coordinator.recordForwardCursor !== 'function'
633
+ || typeof coordinator.clearForwardCursor !== 'function'
634
+ || typeof coordinator.pendingForwardAck !== 'function'
635
+ || typeof coordinator.recordForwardAck !== 'function'
636
+ || typeof coordinator.clearForwardAck !== 'function') {
637
+ throw new Error('Forward cursor coordinator is unavailable');
638
+ }
639
+ return coordinator;
640
+ }
641
+ async confirmPlainForwardAck(ns, method, ackSeq, groupId = '') {
642
+ const client = this.runtime.client;
643
+ const coordinator = this.forwardCoordinator();
644
+ const generation = this.captureInlineGeneration();
645
+ coordinator.recordForwardAck(ns, ackSeq);
646
+ const params = method === 'message.ack'
647
+ ? {
648
+ seq: ackSeq,
649
+ device_id: client._deviceId,
650
+ slot_id: client._slotId,
651
+ _rpc_background: true,
652
+ }
653
+ : {
654
+ group_id: groupId,
655
+ msg_seq: ackSeq,
656
+ device_id: client._deviceId,
657
+ slot_id: client._slotId,
658
+ _rpc_background: true,
659
+ };
660
+ const result = await client._rpcPipeline.rawCall(method, params, { background: true });
661
+ const actualAckSeq = this.resolveForwardAckSeq(result, ackSeq);
662
+ if (actualAckSeq < ackSeq) {
663
+ throw new Error(`${method} server ACK watermark ${actualAckSeq} is below requested ${ackSeq}`);
664
+ }
665
+ coordinator.clearForwardAck(ns, actualAckSeq);
666
+ if (method === 'message.ack') {
667
+ this.completePendingP2PInlineAckUpTo(ns, actualAckSeq, generation);
668
+ }
669
+ else {
670
+ this.completePendingGroupInlineAckUpTo(ns, actualAckSeq, generation);
671
+ }
672
+ }
673
+ async commitPlainForwardPage(ns, afterSeq, method, result, groupId = '') {
674
+ if (!isJsonObject(result)) {
675
+ throw new Error(`${method} response must be an object`);
676
+ }
677
+ const client = this.runtime.client;
678
+ const response = result;
679
+ const messages = deduplicatePlainForwardPageMessages(response.messages, afterSeq);
680
+ const coordinator = this.forwardCoordinator();
681
+ const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
682
+ const pageInlineGeneration = this.captureInlineGeneration();
683
+ const deferredServerCursor = Number(coordinator.pendingForwardCursor(ns, pageContigBefore) ?? 0);
684
+ const deferredForwardAck = Number(coordinator.pendingForwardAck(ns, pageContigBefore) ?? 0);
685
+ const pageTrackerSnapshot = typeof client._seqTracker.snapshotNamespace === 'function'
686
+ ? client._seqTracker.snapshotNamespace(ns)
687
+ : null;
688
+ response._contig_before = pageContigBefore;
689
+ const cursor = isJsonObject(response.cursor)
690
+ ? response.cursor
691
+ : null;
692
+ const serverAckRaw = method === 'message.pull' ? response.server_ack_seq : cursor?.current_seq;
693
+ const hasServerAck = method === 'message.pull'
694
+ ? Object.prototype.hasOwnProperty.call(response, 'server_ack_seq')
695
+ : Boolean(cursor && Object.prototype.hasOwnProperty.call(cursor, 'current_seq'));
696
+ const parsedServerAck = hasServerAck ? nonNegativeSafeSequenceHint(serverAckRaw) : null;
697
+ const serverAckSeq = parsedServerAck ?? 0;
698
+ const retentionFloor = method === 'message.pull'
699
+ ? positiveSafeSequenceHint(response.retention_floor_seq)
700
+ : Math.max(positiveSafeSequenceHint(response.retention_floor_message_seq), positiveSafeSequenceHint(response.retention_floor_seq), positiveSafeSequenceHint(cursor?.retention_floor_seq));
701
+ const visibilityFloor = method === 'group.pull'
702
+ ? Math.max(positiveSafeSequenceHint(cursor?.join_seq), Math.max(0, positiveSafeSequenceHint(response.earliest_available_message_seq) - 1))
703
+ : 0;
704
+ let publishedCount = 0;
705
+ let committed = false;
706
+ try {
707
+ for (const rawMessage of messages) {
708
+ const seq = positiveSafeSequenceHint(rawMessage.seq);
709
+ if (method === 'message.pull') {
710
+ const appEvent = p2pAppEventFromPlainPullMessage(rawMessage);
711
+ if (await this.publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false)) {
712
+ publishedCount += 1;
713
+ }
714
+ continue;
715
+ }
716
+ const message = normalizeGroupMentionMode(rawMessage);
717
+ if (this.recallEventFromGroupMessage(message)) {
718
+ if (await this.publishGroupRecallTombstone(groupId, seq, message)) {
719
+ this.markPublishedSeq(ns, seq);
720
+ publishedCount += 1;
721
+ }
722
+ }
723
+ else if (await this.publishPulledMessage('group.message_created', ns, seq, message, false)) {
724
+ publishedCount += 1;
725
+ }
726
+ }
727
+ if (messages.length > 0)
728
+ client._seqTracker.onPullResult(ns, messages, afterSeq);
729
+ const commitTarget = Math.max(client._seqTracker.getContiguousSeq(ns), retentionFloor, visibilityFloor, deferredServerCursor);
730
+ if (commitTarget > client._seqTracker.getContiguousSeq(ns)) {
731
+ client._seqTracker.forceContiguousSeq(ns, commitTarget);
732
+ }
733
+ if (client._seqTracker.getContiguousSeq(ns) !== pageContigBefore) {
734
+ await this.drainOrderedMessages(ns, undefined, false, false);
735
+ await client._commitSeqTrackerState(ns);
736
+ }
737
+ committed = true;
738
+ }
739
+ catch (exc) {
740
+ if (!committed && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
741
+ client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
742
+ this.dropSeqTrackerPending(ns);
743
+ }
744
+ throw exc;
745
+ }
746
+ const committedAck = client._seqTracker.getContiguousSeq(ns);
747
+ if (deferredServerCursor > 0 && committedAck >= deferredServerCursor) {
748
+ coordinator.clearForwardCursor(ns, committedAck);
749
+ }
750
+ if (hasServerAck && parsedServerAck !== null && serverAckSeq > committedAck) {
751
+ coordinator.recordForwardCursor(ns, serverAckSeq, committedAck);
752
+ }
753
+ if (hasServerAck && parsedServerAck !== null && serverAckSeq >= deferredForwardAck) {
754
+ coordinator.clearForwardAck(ns, serverAckSeq);
755
+ if (method === 'message.pull') {
756
+ this.completePendingP2PInlineAckUpTo(ns, serverAckSeq, pageInlineGeneration);
757
+ }
758
+ else {
759
+ this.completePendingGroupInlineAckUpTo(ns, serverAckSeq, pageInlineGeneration);
760
+ }
761
+ }
762
+ const contigAdvanced = committedAck > pageContigBefore;
763
+ const knownServerAck = hasServerAck ? serverAckSeq : deferredServerCursor;
764
+ if (committedAck > 0
765
+ && ((hasServerAck && committedAck > serverAckSeq)
766
+ || (contigAdvanced && committedAck > knownServerAck))) {
767
+ coordinator.recordForwardAck(ns, committedAck);
768
+ }
769
+ const pendingAckSeq = Number(coordinator.pendingForwardAck(ns, committedAck) ?? 0);
770
+ if (pendingAckSeq > 0) {
771
+ const ackMethod = method === 'message.pull' ? 'message.ack' : 'group.ack_messages';
772
+ const clampedAckSeq = this.clampAckSeq(ackMethod, ackMethod === 'message.ack' ? 'seq' : 'msg_seq', ns, pendingAckSeq);
773
+ if (clampedAckSeq < pendingAckSeq) {
774
+ throw new Error(`${ackMethod} cannot confirm pending Forward watermark ${pendingAckSeq}`);
775
+ }
776
+ await this.confirmPlainForwardAck(ns, ackMethod, pendingAckSeq, groupId);
777
+ }
778
+ return { rawCount: messages.length, publishedCount };
121
779
  }
122
780
  enqueueOrderedMessage(ns, event, seq, payload) {
123
781
  const client = this.runtime.client;
@@ -136,7 +794,7 @@ export class MessageDeliveryEngine {
136
794
  isGroupEventNamespace(ns) {
137
795
  return ns.startsWith('group_event:');
138
796
  }
139
- async publishOrderedQueueItem(ns, event, seq, payload) {
797
+ async publishOrderedQueueItem(ns, event, seq, payload, pullResponse = false) {
140
798
  const client = this.runtime.client;
141
799
  if (event === 'group.changed' && this.isGroupEventNamespace(ns)) {
142
800
  await this.publishOrderedGroupChanged(payload);
@@ -146,6 +804,10 @@ export class MessageDeliveryEngine {
146
804
  await this.publishMessageRecallTombstone(seq, payload);
147
805
  return;
148
806
  }
807
+ if (pullResponse) {
808
+ await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload));
809
+ return;
810
+ }
149
811
  await client._publishAppEvent(event, payload);
150
812
  }
151
813
  async publishOrderedGroupChanged(payload) {
@@ -154,8 +816,8 @@ export class MessageDeliveryEngine {
154
816
  const eventPayload = payload;
155
817
  client._groupState?.handleGroupChangedV2Membership?.(eventPayload);
156
818
  if (eventPayload.action === 'dissolved') {
157
- const rawGroupId = eventPayload.group_aid ?? eventPayload.group_id ?? '';
158
- const groupId = normalizeGroupId(String(rawGroupId)) || String(rawGroupId).trim();
819
+ const rawGroupId = eventPayload.group_id ?? eventPayload.group_aid ?? '';
820
+ const groupId = String(rawGroupId).trim();
159
821
  if (groupId)
160
822
  client._cleanupDissolvedGroup?.(groupId);
161
823
  }
@@ -185,13 +847,28 @@ export class MessageDeliveryEngine {
185
847
  }
186
848
  normalizePublishedMessagePayload(event, payload) {
187
849
  if (this.isInstanceScopedMessageEvent(event)) {
850
+ payload = this.stripInlineInternalFields(payload);
851
+ if (event === 'group.message_created')
852
+ payload = normalizeGroupMentionMode(payload);
188
853
  return this.attachAppMessageEnvelope(this.stripInternalSenderDeviceFields(this.attachCurrentInstanceContext(payload)));
189
854
  }
190
855
  if (this.isGroupScopedEvent(event)) {
856
+ payload = this.stripInlineInternalFields(payload);
191
857
  return this.attachAppGroupEventEnvelope(this.attachCurrentInstanceContext(payload));
192
858
  }
193
859
  return payload;
194
860
  }
861
+ stripInlineInternalFields(payload) {
862
+ if (!isJsonObject(payload))
863
+ return payload;
864
+ if (!Object.prototype.hasOwnProperty.call(payload, 'inline_message')
865
+ && !Object.prototype.hasOwnProperty.call(payload, '_inline_message_candidates_v1'))
866
+ return payload;
867
+ const result = { ...payload };
868
+ delete result.inline_message;
869
+ delete result._inline_message_candidates_v1;
870
+ return result;
871
+ }
195
872
  envelopeMetadata(value) {
196
873
  let source = value;
197
874
  if (source && typeof source === 'object') {
@@ -248,6 +925,7 @@ export class MessageDeliveryEngine {
248
925
  if (protectedHeaders)
249
926
  envelope.protected_headers = protectedHeaders;
250
927
  setIfPresent('payload_type', firstValue(message.payload_type, protectedHeaders?.payload_type));
928
+ setIfPresent('mention_mode', message.mention_mode);
251
929
  return envelope;
252
930
  }
253
931
  isGroupScopedEvent(event) {
@@ -314,19 +992,30 @@ export class MessageDeliveryEngine {
314
992
  if (protectedHeaders)
315
993
  envelope.protected_headers = protectedHeaders;
316
994
  setIfPresent('payload_type', firstValue(params.payload_type, protectedHeaders?.payload_type, body.type));
995
+ if (method === 'group.send')
996
+ setIfPresent('mention_mode', resultObj.mention_mode);
317
997
  return envelope;
318
998
  }
319
- attachSendResultEnvelope(method, params, result, encrypted) {
999
+ attachSendResultEnvelope(method, params, result, encrypted, modeSource) {
320
1000
  if (!APP_SEND_ENVELOPE_METHODS.has(method) || !isJsonObject(result))
321
1001
  return result;
322
- const out = { ...result };
323
- out.envelope = this.sendResultEnvelope(method, params, out, encrypted);
1002
+ let out = { ...result };
1003
+ const serverEnvelope = method === 'group.send' && isJsonObject(out.envelope)
1004
+ ? { ...out.envelope }
1005
+ : undefined;
324
1006
  if ('payload' in params) {
325
1007
  out.payload = params.payload;
326
1008
  }
327
1009
  else if ('content' in params) {
328
1010
  out.payload = params.content;
329
1011
  }
1012
+ const generatedEnvelope = this.sendResultEnvelope(method, params, out, encrypted);
1013
+ out.envelope = serverEnvelope ? { ...generatedEnvelope, ...serverEnvelope } : generatedEnvelope;
1014
+ if (method === 'group.send') {
1015
+ const serverSource = modeSource ?? result;
1016
+ const authoritativeSource = hasGroupMentionModeField(serverSource) ? serverSource : out;
1017
+ out = normalizeGroupMentionMode(out, authoritativeSource);
1018
+ }
330
1019
  return out;
331
1020
  }
332
1021
  attachAppGroupEventEnvelope(payload) {
@@ -516,7 +1205,7 @@ export class MessageDeliveryEngine {
516
1205
  // (group_id, sorted message_ids) 已能唯一标识一次撤回;缺 message_ids 时按原消息 id/seq 兜底。
517
1206
  // 去重键不含 recalled_at:占位/通知 tombstone(pull)与在线 push 三条通道对同一次撤回
518
1207
  // 可能携带不同来源的时间戳(push 在事务后重取),纳入 recalled_at 会使去重失效、回调多次。
519
- const normalizedGroupId = normalizeGroupId(groupId) || String(groupId ?? '').trim();
1208
+ const normalizedGroupId = String(groupId ?? '').trim();
520
1209
  const ids = payload.message_ids;
521
1210
  const idPart = Array.isArray(ids)
522
1211
  ? ids.map((i) => String(i ?? '').trim()).filter(Boolean).sort().join(',')
@@ -545,9 +1234,7 @@ export class MessageDeliveryEngine {
545
1234
  if (!eventPayload)
546
1235
  return false;
547
1236
  const rawGroupId = String(eventPayload.group_id ?? groupId ?? '').trim();
548
- const dedupGroupId = normalizeGroupId(rawGroupId) || rawGroupId;
549
- if (dedupGroupId)
550
- eventPayload.group_id = dedupGroupId;
1237
+ const dedupGroupId = rawGroupId;
551
1238
  const dedupKey = this.groupRecallDedupKey(dedupGroupId, eventPayload);
552
1239
  let seen = client._groupRecallSeen;
553
1240
  if (!seen) {
@@ -645,7 +1332,7 @@ export class MessageDeliveryEngine {
645
1332
  }
646
1333
  if (isJsonObject(payload)) {
647
1334
  try {
648
- const snapshot = client._agentMdManager.eventSnapshot();
1335
+ const snapshot = await client._agentMdManager.eventSnapshot();
649
1336
  const localEtag = snapshot?.local_etag || '';
650
1337
  const remoteEtag = snapshot?.remote_etag || '';
651
1338
  if ((localEtag || remoteEtag) && payload._agent_md === undefined) {
@@ -665,24 +1352,76 @@ export class MessageDeliveryEngine {
665
1352
  if (!isJsonObject(message))
666
1353
  return true;
667
1354
  const client = this.runtime.client;
668
- if ('device_id' in message) {
669
- const targetDeviceId = String(message.device_id ?? '').trim();
670
- if (targetDeviceId !== client._deviceId) {
1355
+ const explicitDevice = this.strictTargetString(message, 'target_device_id');
1356
+ if (explicitDevice.present) {
1357
+ if (!explicitDevice.valid || explicitDevice.value !== client._deviceId)
1358
+ return false;
1359
+ }
1360
+ else {
1361
+ const targetDeviceId = this.boundString(message, ['device_id']);
1362
+ if (targetDeviceId.present && targetDeviceId.value !== client._deviceId) {
671
1363
  return false;
672
1364
  }
673
1365
  }
674
- if ('slot_id' in message) {
675
- const targetSlotId = String(message.slot_id ?? '').trim();
676
- if (slotIsolationKey(targetSlotId) !== slotIsolationKey(client._slotId)) {
1366
+ const explicitSlot = this.strictTargetString(message, 'target_slot_id');
1367
+ if (explicitSlot.present) {
1368
+ if (!explicitSlot.valid
1369
+ || slotIsolationKey(explicitSlot.value) !== slotIsolationKey(client._slotId))
1370
+ return false;
1371
+ }
1372
+ else {
1373
+ const targetSlotId = this.boundString(message, ['slot_id']);
1374
+ if (targetSlotId.present
1375
+ && slotIsolationKey(targetSlotId.value) !== slotIsolationKey(client._slotId)) {
677
1376
  return false;
678
1377
  }
679
1378
  }
680
1379
  return true;
681
1380
  }
682
- async runPushProcessSerialized(ns, operation) {
1381
+ strictTargetString(source, key) {
1382
+ if (!Object.prototype.hasOwnProperty.call(source, key)) {
1383
+ return { present: false, valid: false, value: '' };
1384
+ }
1385
+ const raw = source[key];
1386
+ if (typeof raw !== 'string')
1387
+ return { present: true, valid: false, value: '' };
1388
+ const value = raw.trim();
1389
+ return { present: true, valid: Boolean(value), value };
1390
+ }
1391
+ async runPushProcessSerialized(ns, operation, inlineGeneration) {
683
1392
  const client = this.runtime.client;
684
- if (!ns) {
1393
+ if (inlineGeneration === undefined) {
1394
+ if (!ns) {
1395
+ await operation();
1396
+ return;
1397
+ }
1398
+ let queues = client._pushProcessQueues;
1399
+ if (!queues) {
1400
+ queues = new Map();
1401
+ client._pushProcessQueues = queues;
1402
+ }
1403
+ const previous = queues.get(ns) ?? Promise.resolve();
1404
+ const current = previous.catch(() => undefined).then(operation);
1405
+ const stored = current.then(() => undefined, () => undefined);
1406
+ queues.set(ns, stored);
1407
+ try {
1408
+ await current;
1409
+ }
1410
+ finally {
1411
+ if (queues.get(ns) === stored)
1412
+ queues.delete(ns);
1413
+ }
1414
+ return;
1415
+ }
1416
+ const run = async () => {
1417
+ if (!this.isInlineGenerationCurrent(inlineGeneration))
1418
+ return;
685
1419
  await operation();
1420
+ if (!this.isInlineGenerationCurrent(inlineGeneration))
1421
+ return;
1422
+ };
1423
+ if (!ns) {
1424
+ await run();
686
1425
  return;
687
1426
  }
688
1427
  let queues = client._pushProcessQueues;
@@ -691,7 +1430,7 @@ export class MessageDeliveryEngine {
691
1430
  client._pushProcessQueues = queues;
692
1431
  }
693
1432
  const previous = queues.get(ns) ?? Promise.resolve();
694
- const current = previous.catch(() => undefined).then(operation);
1433
+ const current = previous.catch(() => undefined).then(run);
695
1434
  const stored = current.then(() => undefined, () => undefined);
696
1435
  queues.set(ns, stored);
697
1436
  try {
@@ -708,7 +1447,18 @@ export class MessageDeliveryEngine {
708
1447
  const ns = isJsonObject(data) && client._aid && data.seq !== undefined
709
1448
  ? `p2p:${client._aid}`
710
1449
  : '';
711
- client._safeAsync(this.runPushProcessSerialized(ns, () => this.processAndPublishMessage(data)));
1450
+ const seq = isJsonObject(data) && typeof data.seq === 'number' ? data.seq : 0;
1451
+ if (client._v2Session && ns && Number.isSafeInteger(seq) && seq > 0
1452
+ && this.messageTargetsCurrentInstance(data)) {
1453
+ client._seqTracker.updateMaxSeen(ns, seq);
1454
+ }
1455
+ const hasInlineObject = isJsonObject(data)
1456
+ && isJsonObject(data.inline_message);
1457
+ const inlineGeneration = hasInlineObject ? this.captureInlineGeneration() : undefined;
1458
+ const operation = () => this.processAndPublishMessage(data, inlineGeneration);
1459
+ client._safeAsync(client._v2Session && ns && !hasInlineObject
1460
+ ? operation()
1461
+ : this.runPushProcessSerialized(ns, operation, inlineGeneration));
712
1462
  client._clientLog.debug('_onRawMessageReceived exit: elapsed=0ms (dispatched async)');
713
1463
  }
714
1464
  async onRawMessageRecalled(data) {
@@ -729,7 +1479,7 @@ export class MessageDeliveryEngine {
729
1479
  const seqNeedsPull = client._seqTracker.onMessageSeq(ns, seq);
730
1480
  const published = await this.publishOrderedMessage('message.recalled', ns, seq, msg);
731
1481
  const contigAfter = client._seqTracker.getContiguousSeq(ns);
732
- if (seqNeedsPull && !published) {
1482
+ if (seqNeedsPull && !published && client._sessionOptions?.background_sync !== false) {
733
1483
  client._safeAsync(this.fillP2pGap());
734
1484
  }
735
1485
  const contig = client._seqTracker.getContiguousSeq(ns);
@@ -748,7 +1498,7 @@ export class MessageDeliveryEngine {
748
1498
  }
749
1499
  await this.publishMessageRecallTombstone(seq, msg);
750
1500
  }
751
- async processAndPublishMessage(data) {
1501
+ async processAndPublishMessage(data, inlineGeneration) {
752
1502
  const client = this.runtime.client;
753
1503
  try {
754
1504
  if (!isJsonObject(data)) {
@@ -759,6 +1509,15 @@ export class MessageDeliveryEngine {
759
1509
  if (!this.messageTargetsCurrentInstance(msg)) {
760
1510
  return;
761
1511
  }
1512
+ // 新 SDK 的所有带合法 seq P2P Push 都只作为 Head 通知处理。
1513
+ // 新服务端为兼容旧 SDK 仍可能发送 message.received;V2 会话下必须
1514
+ // 在旧 seq tracking / ordered publish 之前转入前台 Tail 协调器。
1515
+ const legacyPushSeq = positiveSafeSequenceHint(msg.seq);
1516
+ const hasInlineField = Object.prototype.hasOwnProperty.call(msg, 'inline_message');
1517
+ if (client._v2Session && client._aid && (legacyPushSeq > 0 || hasInlineField)) {
1518
+ await this.processV2P2PNotification(msg, 'v1', inlineGeneration);
1519
+ return;
1520
+ }
762
1521
  const seq = msg.seq;
763
1522
  const encryptedPush = client._isEncryptedPushMessage(msg);
764
1523
  if (seq !== undefined && seq !== null && client._aid) {
@@ -772,7 +1531,7 @@ export class MessageDeliveryEngine {
772
1531
  : await this.publishOrderedMessage('message.received', ns, seq, msg);
773
1532
  const contigAfter = client._seqTracker.getContiguousSeq(ns);
774
1533
  const needPull = seqNeedsPull && !published;
775
- if (needPull) {
1534
+ if (needPull && client._sessionOptions?.background_sync !== false) {
776
1535
  client._safeAsync(this.fillP2pGap());
777
1536
  }
778
1537
  const contig = client._seqTracker.getContiguousSeq(ns);
@@ -818,11 +1577,24 @@ export class MessageDeliveryEngine {
818
1577
  onRawGroupMessageCreated(data) {
819
1578
  const client = this.runtime.client;
820
1579
  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)));
1580
+ const groupId = isJsonObject(data) && typeof data.group_id === 'string' ? data.group_id.trim() : '';
1581
+ const seq = isJsonObject(data) && typeof data.seq === 'number' ? data.seq : 0;
1582
+ const kind = isJsonObject(data) ? String(data.kind ?? '').trim() : '';
1583
+ if (client._v2Session && groupId && kind !== 'group.online_unread_hint'
1584
+ && Number.isSafeInteger(seq) && seq > 0) {
1585
+ client._seqTracker.updateMaxSeen(`group:${groupId}`, seq);
1586
+ }
1587
+ const ns = groupId ? `group:${groupId}` : '';
1588
+ const hasInlineObject = isJsonObject(data)
1589
+ && isJsonObject(data.inline_message);
1590
+ const inlineGeneration = hasInlineObject ? this.captureInlineGeneration() : undefined;
1591
+ const operation = () => this.processAndPublishGroupMessage(data, inlineGeneration);
1592
+ client._safeAsync(client._v2Session && ns && !hasInlineObject
1593
+ ? operation()
1594
+ : this.runPushProcessSerialized(ns, operation, inlineGeneration));
823
1595
  client._clientLog.debug('_onRawGroupMessageCreated exit: elapsed=0ms (dispatched async)');
824
1596
  }
825
- async processAndPublishGroupMessage(data) {
1597
+ async processAndPublishGroupMessage(data, inlineGeneration) {
826
1598
  const client = this.runtime.client;
827
1599
  try {
828
1600
  if (!isJsonObject(data)) {
@@ -830,12 +1602,20 @@ export class MessageDeliveryEngine {
830
1602
  return;
831
1603
  }
832
1604
  const msg = { ...data };
833
- const groupId = (msg.group_id ?? '');
1605
+ const groupId = typeof msg.group_id === 'string' ? msg.group_id.trim() : '';
834
1606
  const seq = msg.seq;
835
1607
  const payload = msg.payload;
836
1608
  if (groupId) {
837
1609
  client._groupSynced.add(groupId);
838
1610
  }
1611
+ // V2 会话中旧 group.message_created 也只是 Head 通知;必须先 Tail,
1612
+ // 不能让 payload 路径直接推进旧 ordered publish 状态。
1613
+ const legacyPushSeq = positiveSafeSequenceHint(seq);
1614
+ const hasInlineField = Object.prototype.hasOwnProperty.call(msg, 'inline_message');
1615
+ if (client._v2Session && groupId && (legacyPushSeq > 0 || hasInlineField)) {
1616
+ await this.processGroupV2MessageCreated(msg, 'v1', inlineGeneration);
1617
+ return;
1618
+ }
839
1619
  if (payload === undefined || payload === null
840
1620
  || (typeof payload === 'object' && Object.keys(payload).length === 0)) {
841
1621
  await this.autoPullGroupMessages(msg);
@@ -873,7 +1653,7 @@ export class MessageDeliveryEngine {
873
1653
  : await this.publishOrderedMessage('group.message_created', ns, seq, msg);
874
1654
  const contigAfter = client._seqTracker.getContiguousSeq(ns);
875
1655
  const needPull = seqNeedsPull && !published;
876
- if (needPull) {
1656
+ if (needPull && client._sessionOptions?.background_sync !== false) {
877
1657
  client._safeAsync(this.fillGroupGap(groupId));
878
1658
  }
879
1659
  const contig = client._seqTracker.getContiguousSeq(ns);
@@ -924,56 +1704,53 @@ export class MessageDeliveryEngine {
924
1704
  await client._publishAppEvent('group.message_created', notification);
925
1705
  return;
926
1706
  }
927
- const ns = `group:${groupId}`;
1707
+ if (client._sessionOptions?.background_sync === false) {
1708
+ client._clientLog.debug(`auto pull group messages skipped: group=${groupId} background_sync=false`);
1709
+ return;
1710
+ }
1711
+ const ns = this.resolvePendingGroupInlineAckNamespace(`group:${groupId}`);
928
1712
  const afterSeq = client._seqTracker.getContiguousSeq(ns);
929
- try {
930
- if (client._v2Session) {
931
- await client._withBackgroundRpc(() => client._pullGroupV2Internal({ group_id: groupId, after_seq: afterSeq, limit: 50 }));
1713
+ if (client._v2Session) {
1714
+ try {
1715
+ const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 });
1716
+ await client._rpcPipeline.runPullSerialized(key, () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true }), true);
1717
+ return;
1718
+ }
1719
+ catch (exc) {
1720
+ client._clientLog.warn(`auto pull group message failed:${String(exc)}`);
1721
+ await client._publishAppEvent('group.message_created', notification);
932
1722
  return;
933
1723
  }
934
- const result = await client.call('group.pull', {
1724
+ }
1725
+ let rawCompleted = false;
1726
+ try {
1727
+ const key = client._rpcPipeline.pullGateKeyForCall('group.pull', {
935
1728
  group_id: groupId,
936
1729
  after_message_seq: afterSeq,
937
- device_id: client._deviceId,
938
1730
  limit: 50,
939
- _rpc_background: true,
940
1731
  });
941
- if (isJsonObject(result)) {
942
- const messages = result.messages;
943
- if (Array.isArray(messages)) {
944
- const pushed = client._pushedSeqs.get(ns);
945
- for (const msg of messages) {
946
- if (isJsonObject(msg)) {
947
- const s = msg.seq;
948
- if (pushed && s !== undefined && s !== null && pushed.has(s)) {
949
- continue;
950
- }
951
- // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
952
- // 与 V2 pull 路径(v2-e2ee.ts pullGroupV2)对齐,避免 legacy 回退路径把撤回
953
- // tombstone 当普通 group.message_created 投递给应用层。
954
- if (s !== undefined && s !== null
955
- && this.recallEventFromGroupMessage(msg)) {
956
- await this.publishGroupRecallTombstone(groupId, s, msg);
957
- this.markPublishedSeq(ns, Number(s));
958
- continue;
959
- }
960
- if (s !== undefined && s !== null) {
961
- await client._publishPulledMessage('group.message_created', ns, s, msg);
962
- }
963
- else {
964
- await client._publishAppEvent('group.message_created', msg);
965
- }
966
- }
967
- }
968
- this.prunePushedSeqs(ns);
969
- return;
970
- }
971
- }
972
- }
973
- catch (exc) {
974
- client._clientLog.warn(`auto pull group message failed:${String(exc)}`);
1732
+ await client._rpcPipeline.runPullSerialized(key, async () => {
1733
+ const result = await client._rpcPipeline.rawCall('group.pull', {
1734
+ group_id: groupId,
1735
+ after_message_seq: afterSeq,
1736
+ device_id: client._deviceId,
1737
+ slot_id: client._slotId,
1738
+ limit: 50,
1739
+ _rpc_background: true,
1740
+ }, { background: true });
1741
+ rawCompleted = true;
1742
+ await this.commitPlainForwardPage(ns, afterSeq, 'group.pull', result, groupId);
1743
+ this.prunePushedSeqs(ns);
1744
+ }, true);
1745
+ }
1746
+ catch (exc) {
1747
+ // 页发布或本地提交失败不能 ACK;提交后 ACK 失败则保留 pending。
1748
+ // 两种情况都不能再把原始通知当作第二条业务消息发布。
1749
+ client._clientLog.warn(`auto pull group message commit/ack failed:${String(exc)}`);
1750
+ if (!rawCompleted) {
1751
+ await client._publishAppEvent('group.message_created', notification);
1752
+ }
975
1753
  }
976
- await client._publishAppEvent('group.message_created', notification);
977
1754
  }
978
1755
  scheduleP2pGapRetry(ns, retryAttempt) {
979
1756
  const client = this.runtime.client;
@@ -994,6 +1771,8 @@ export class MessageDeliveryEngine {
994
1771
  }
995
1772
  async fillP2pGap(retryAttempt = 0) {
996
1773
  const client = this.runtime.client;
1774
+ if (client._sessionOptions?.background_sync === false)
1775
+ return;
997
1776
  if (client.state !== ConnectionState.READY || client._closing)
998
1777
  return;
999
1778
  if (!client._aid)
@@ -1007,7 +1786,8 @@ export class MessageDeliveryEngine {
1007
1786
  this.runtime.delivery.setGapFillActive(true);
1008
1787
  let filled = 0;
1009
1788
  try {
1010
- const messages = await client._withBackgroundRpc(() => client._pullV2(afterSeq, 50));
1789
+ const key = client._rpcPipeline.pullGateKeyForCall('message.pull', { after_seq: afterSeq, limit: 50 });
1790
+ const messages = await client._rpcPipeline.runPullSerialized(key, () => client._pullV2(afterSeq, 50, { gateLocked: true }), true);
1011
1791
  filled = messages.length;
1012
1792
  this.prunePushedSeqs(ns);
1013
1793
  }
@@ -1031,6 +1811,7 @@ export class MessageDeliveryEngine {
1031
1811
  finally {
1032
1812
  client._gapFillDone.delete(dedupKey);
1033
1813
  this.runtime.delivery.setGapFillActive(false);
1814
+ this.schedulePendingPullIfNeeded(ns, 'gap-fill-complete');
1034
1815
  }
1035
1816
  }
1036
1817
  async fillP2pGapWithPlainPull(ns, afterSeq) {
@@ -1042,31 +1823,17 @@ export class MessageDeliveryEngine {
1042
1823
  slot_id: client._slotId,
1043
1824
  _rpc_background: true,
1044
1825
  };
1045
- const raw = await client._transport.call('message.pull', params, undefined, undefined, true);
1046
- const result = typeof client._rpcPipeline?.postprocessResult === 'function'
1047
- ? await client._rpcPipeline.postprocessResult('message.pull', params, raw)
1048
- : raw;
1049
- const messages = (isJsonObject(result) && Array.isArray(result.messages))
1050
- ? result.messages
1051
- : [];
1052
- let published = 0;
1053
- for (const item of messages) {
1054
- if (!isJsonObject(item))
1055
- continue;
1056
- const msg = item;
1057
- const seq = Number(msg.seq ?? 0);
1058
- const appEvent = p2pAppEventFromPlainPullMessage(msg);
1059
- if (await this.publishPulledMessage(appEvent.event, ns, seq, appEvent.payload)) {
1060
- published += 1;
1061
- }
1062
- }
1063
- return published;
1826
+ const raw = await client._rpcPipeline.rawCall('message.pull', params, { background: true });
1827
+ const page = await this.commitPlainForwardPage(ns, afterSeq, 'message.pull', raw);
1828
+ return page.publishedCount;
1064
1829
  }
1065
1830
  async fillGroupGap(groupId) {
1066
1831
  const client = this.runtime.client;
1832
+ if (client._sessionOptions?.background_sync === false)
1833
+ return;
1067
1834
  if (client.state !== ConnectionState.READY || client._closing)
1068
1835
  return;
1069
- groupId = normalizeGroupId(groupId) || String(groupId ?? '').trim();
1836
+ groupId = String(groupId ?? '').trim();
1070
1837
  if (!groupId)
1071
1838
  return;
1072
1839
  const ns = `group:${groupId}`;
@@ -1078,7 +1845,8 @@ export class MessageDeliveryEngine {
1078
1845
  this.runtime.delivery.setGapFillActive(true);
1079
1846
  let filled = 0;
1080
1847
  try {
1081
- const messages = await client._withBackgroundRpc(() => client._pullGroupV2(groupId, afterSeq, 50));
1848
+ const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 });
1849
+ const messages = await client._rpcPipeline.runPullSerialized(key, () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true }), true);
1082
1850
  filled = messages.length;
1083
1851
  this.prunePushedSeqs(ns);
1084
1852
  }
@@ -1091,35 +1859,44 @@ export class MessageDeliveryEngine {
1091
1859
  if (filled > 0 && client._seqTracker.getContiguousSeq(ns) > afterSeq) {
1092
1860
  client._safeAsync(this.fillGroupGap(groupId));
1093
1861
  }
1862
+ this.schedulePendingPullIfNeeded(ns, 'gap-fill-complete');
1094
1863
  }
1095
1864
  }
1096
- async fillGroupEventGap(groupId) {
1865
+ async fillGroupEventGap(groupId, options) {
1097
1866
  const client = this.runtime.client;
1867
+ if (client._sessionOptions?.background_sync === false)
1868
+ return;
1098
1869
  if (client.state !== ConnectionState.READY || client._closing)
1099
1870
  return;
1100
- groupId = normalizeGroupId(groupId) || String(groupId ?? '').trim();
1871
+ groupId = String(groupId ?? '').trim();
1101
1872
  if (!groupId)
1102
1873
  return;
1103
1874
  const ns = `group_event:${groupId}`;
1104
- const afterSeq = client._seqTracker.getContiguousSeq(ns);
1875
+ const requestedAfterSeq = Number(options?.afterSeq ?? 0);
1876
+ const afterSeq = Number.isSafeInteger(requestedAfterSeq) && requestedAfterSeq > 0
1877
+ ? requestedAfterSeq
1878
+ : client._seqTracker.getContiguousSeq(ns);
1879
+ const singlePage = options?.singlePage === true;
1105
1880
  const dedupKey = `group_event_pull:${ns}`;
1106
1881
  if (client._gapFillDone.has(dedupKey))
1107
1882
  return;
1108
1883
  client._gapFillDone.add(dedupKey);
1109
1884
  this.runtime.delivery.setGapFillActive(true);
1885
+ let continuationAfterSeq = 0;
1110
1886
  try {
1111
1887
  let nextAfterSeq = afterSeq;
1112
- const maxPages = 100;
1888
+ const maxPages = singlePage ? 1 : 100;
1113
1889
  let pageCount = 0;
1114
1890
  while (pageCount < maxPages) {
1115
1891
  pageCount += 1;
1116
- const result = await client.call('group.pull_events', {
1892
+ const request = {
1117
1893
  group_id: groupId,
1118
1894
  after_event_seq: nextAfterSeq,
1119
1895
  device_id: client._deviceId,
1120
1896
  limit: 50,
1121
1897
  _rpc_background: true,
1122
- });
1898
+ };
1899
+ const result = await client.call('group.pull_events', request);
1123
1900
  if (!isJsonObject(result))
1124
1901
  return;
1125
1902
  const events = result.events;
@@ -1131,12 +1908,16 @@ export class MessageDeliveryEngine {
1131
1908
  client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq);
1132
1909
  }
1133
1910
  const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1134
- const serverAck = cursor ? Number(cursor.current_seq ?? 0) : 0;
1135
- if (serverAck > 0) {
1911
+ const retentionFloor = Math.max(0, Number(result.retention_floor_event_seq ?? 0), Number(cursor?.retention_floor_seq ?? 0));
1912
+ const serverAckFloor = Math.max(0, Number(cursor?.current_seq ?? 0));
1913
+ const earliestAvailable = Number(result.earliest_available_event_seq ?? 0);
1914
+ const visibilityFloor = Math.max(0, Number(cursor?.join_seq ?? 0), Number.isFinite(earliestAvailable) && earliestAvailable > 0 ? earliestAvailable - 1 : 0);
1915
+ const effectiveFloor = Math.max(...[retentionFloor, serverAckFloor, visibilityFloor].filter((value) => Number.isFinite(value)));
1916
+ if (effectiveFloor > 0) {
1136
1917
  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);
1918
+ if (contigBeforeFloor < effectiveFloor) {
1919
+ client._clientLog.info('group.pull_events floor advance: ns=' + ns + ' contiguous=' + contigBeforeFloor + ' -> effective_floor=' + effectiveFloor + ' retention_floor=' + retentionFloor + ' server_ack=' + serverAckFloor + ' visibility_floor=' + visibilityFloor);
1920
+ client._seqTracker.forceContiguousSeq(ns, effectiveFloor);
1140
1921
  }
1141
1922
  }
1142
1923
  const eventSeqs = [];
@@ -1171,17 +1952,28 @@ export class MessageDeliveryEngine {
1171
1952
  this.persistSeq(ns);
1172
1953
  }
1173
1954
  if (eventObjects.length > 0 && ackContig > 0 && ackContig !== pageContigBefore) {
1174
- const ackSeq = this.clampAckSeq('group.ack_events', 'event_seq', ns, ackContig);
1175
- client._transport.call('group.ack_events', {
1176
- group_id: groupId,
1177
- event_seq: ackSeq,
1178
- device_id: client._deviceId,
1179
- slot_id: client._slotId,
1180
- _rpc_background: true,
1181
- }).catch((e) => { client._clientLog.warn('group event auto-ack failed: group=' + groupId, e); });
1955
+ try {
1956
+ const ackPromise = client._transport.call('group.ack_events', {
1957
+ group_id: groupId,
1958
+ event_seq: ackContig,
1959
+ device_id: client._deviceId,
1960
+ slot_id: client._slotId,
1961
+ _rpc_background: true,
1962
+ });
1963
+ if (singlePage)
1964
+ await ackPromise;
1965
+ else
1966
+ ackPromise.catch((e) => { client._clientLog.warn('group event auto-ack failed: group=' + groupId, e); });
1967
+ }
1968
+ catch (e) {
1969
+ client._clientLog.warn('group event auto-ack failed: group=' + groupId, e);
1970
+ }
1182
1971
  }
1183
1972
  const nextAfter = Math.max(eventSeqs.length > 0 ? Math.max(...eventSeqs) : nextAfterSeq, nextAfterSeq);
1184
- if (eventObjects.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
1973
+ if (singlePage && result.has_more === true && nextAfter > nextAfterSeq) {
1974
+ continuationAfterSeq = nextAfter;
1975
+ }
1976
+ if (eventObjects.length === 0 || nextAfter <= nextAfterSeq || result.has_more !== true)
1185
1977
  break;
1186
1978
  nextAfterSeq = nextAfter;
1187
1979
  }
@@ -1195,11 +1987,17 @@ export class MessageDeliveryEngine {
1195
1987
  finally {
1196
1988
  client._gapFillDone.delete(dedupKey);
1197
1989
  this.runtime.delivery.setGapFillActive(false);
1990
+ if (singlePage && continuationAfterSeq > afterSeq) {
1991
+ this.enqueueOnlineUnreadEventHint({
1992
+ group_id: groupId,
1993
+ _online_hint_after_event_seq: continuationAfterSeq,
1994
+ });
1995
+ }
1198
1996
  }
1199
1997
  }
1200
1998
  async handleGroupChangedEventSeq(data, groupId) {
1201
1999
  const client = this.runtime.client;
1202
- groupId = normalizeGroupId(groupId) || String(groupId ?? '').trim();
2000
+ groupId = String(groupId ?? '').trim();
1203
2001
  let needPull = false;
1204
2002
  const rawEventSeq = data.event_seq;
1205
2003
  const eventSeq = Number(rawEventSeq);
@@ -1208,6 +2006,15 @@ export class MessageDeliveryEngine {
1208
2006
  return;
1209
2007
  }
1210
2008
  const ns = `group_event:${groupId}`;
2009
+ if (String(data.kind ?? '').trim() === 'group.online_unread_hint') {
2010
+ if (client._sessionOptions?.background_sync === false) {
2011
+ client._clientLog.debug(`group.changed online unread hint skipped: group=${groupId} background_sync=false`);
2012
+ return;
2013
+ }
2014
+ client._clientLog.debug(`group.changed online unread hint triggering event Forward: group=${groupId}, event_seq=${eventSeq}`);
2015
+ this.enqueueOnlineUnreadEventHint(data);
2016
+ return;
2017
+ }
1211
2018
  if (this.isSelfJoinGroupChanged(data)) {
1212
2019
  const contig = client._seqTracker.getContiguousSeq(ns);
1213
2020
  const maxSeen = client._seqTracker.getMaxSeenSeq(ns);
@@ -1235,10 +2042,9 @@ export class MessageDeliveryEngine {
1235
2042
  if (ackContig > 0 && ackContig !== contigBefore) {
1236
2043
  if (data.action !== 'dissolved')
1237
2044
  this.persistSeq(ns);
1238
- const ackSeq = this.clampAckSeq('group.ack_events', 'event_seq', ns, ackContig);
1239
2045
  client._transport.call('group.ack_events', {
1240
2046
  group_id: groupId,
1241
- event_seq: ackSeq,
2047
+ event_seq: ackContig,
1242
2048
  device_id: client._deviceId,
1243
2049
  slot_id: client._slotId,
1244
2050
  _rpc_background: true,
@@ -1246,7 +2052,7 @@ export class MessageDeliveryEngine {
1246
2052
  client._clientLog.warn('group event push auto-ack failed: group=' + groupId, e);
1247
2053
  });
1248
2054
  }
1249
- if (needPull && groupId && !data._from_gap_fill) {
2055
+ if (needPull && groupId && !data._from_gap_fill && client._sessionOptions?.background_sync !== false) {
1250
2056
  client._safeAsync(this.fillGroupEventGap(groupId));
1251
2057
  }
1252
2058
  }
@@ -1268,185 +2074,882 @@ export class MessageDeliveryEngine {
1268
2074
  const groupId = String(data.group_id ?? '').trim();
1269
2075
  if (!groupId)
1270
2076
  return;
1271
- client._onlineUnreadHintQueue.set(groupId, { ...data });
1272
- if (client._onlineUnreadHintTimer || client._onlineUnreadHintDrainActive)
1273
- return;
1274
- const delayMs = Math.max(0, Number(client._onlineUnreadHintInitialDelayMs ?? 750) || 0);
1275
- this.runtime.delivery.setOnlineUnreadHintTimer(setTimeout(() => {
1276
- this.runtime.delivery.setOnlineUnreadHintTimer(null);
1277
- client._safeAsync(this.drainOnlineUnreadHints());
1278
- }, delayMs));
2077
+ const ns = `group:${groupId}`;
2078
+ client._safeAsync(this.runGroupForwardRecovery(groupId, ns, 50, true).catch((exc) => {
2079
+ client._clientLog?.debug(`online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
2080
+ }));
1279
2081
  }
1280
- async drainOnlineUnreadHints() {
2082
+ enqueueOnlineUnreadEventHint(data) {
1281
2083
  const client = this.runtime.client;
1282
- if (client._onlineUnreadHintDrainActive)
2084
+ const groupId = String(data.group_id ?? '').trim();
2085
+ if (!groupId || client._sessionOptions?.background_sync === false)
1283
2086
  return;
1284
- this.runtime.delivery.setOnlineUnreadHintDrainActive(true);
2087
+ const rawAfter = Number(data._online_hint_after_event_seq ?? 0);
2088
+ const afterSeq = Number.isSafeInteger(rawAfter) && rawAfter > 0 ? rawAfter : 0;
2089
+ client._safeAsync(this.fillGroupEventGap(groupId, {
2090
+ afterSeq,
2091
+ singlePage: true,
2092
+ }));
2093
+ }
2094
+ async runP2PForwardRecovery(ns, pageLimit, background = false) {
2095
+ const client = this.runtime.client;
2096
+ const request = {
2097
+ after_seq: this.syncState(ns).ack,
2098
+ limit: pageLimit,
2099
+ max_pages: 1,
2100
+ };
2101
+ const invoke = async () => {
2102
+ const after = this.syncState(ns).ack;
2103
+ const messages = await client._pullV2(after, pageLimit, { gateLocked: true, maxPages: 1 });
2104
+ return { messages, raw_count: messages.length };
2105
+ };
2106
+ const pipeline = client._rpcPipeline;
2107
+ if (!pipeline)
2108
+ return await invoke();
2109
+ const key = pipeline.pullGateKeyForCall('message.pull', request);
2110
+ return await pipeline.runPullSerialized(key, invoke, background);
2111
+ }
2112
+ async runGroupForwardRecovery(groupId, ns, pageLimit, background = false) {
2113
+ const client = this.runtime.client;
2114
+ const request = {
2115
+ group_id: groupId,
2116
+ after_seq: this.syncState(ns).ack,
2117
+ limit: pageLimit,
2118
+ max_pages: 1,
2119
+ };
2120
+ const invoke = async () => {
2121
+ const after = this.syncState(ns).ack;
2122
+ const messages = await client._pullGroupV2(groupId, after, pageLimit, { gateLocked: true, maxPages: 1 });
2123
+ return { messages, raw_count: messages.length };
2124
+ };
2125
+ const pipeline = client._rpcPipeline;
2126
+ if (!pipeline)
2127
+ return await invoke();
2128
+ const key = pipeline.pullGateKeyForCall('group.pull', request);
2129
+ return await pipeline.runPullSerialized(key, invoke, background);
2130
+ }
2131
+ syncState(ns) {
2132
+ const tracker = this.runtime.client._seqTracker;
2133
+ if (typeof tracker.getSyncState === 'function')
2134
+ return tracker.getSyncState(ns);
2135
+ const ack = Number(tracker.getContiguousSeq(ns) || 0);
2136
+ return { ack, tail: ack, head: Math.max(ack, Number(tracker.getMaxSeenSeq?.(ns) || 0)) };
2137
+ }
2138
+ inlineMessage(data) {
2139
+ if (!isJsonObject(data))
2140
+ return null;
2141
+ const inline = data.inline_message;
2142
+ return isJsonObject(inline)
2143
+ ? inline
2144
+ : null;
2145
+ }
2146
+ boundString(source, keys) {
2147
+ for (const key of keys) {
2148
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
2149
+ return { present: true, value: String(source[key] ?? '').trim() };
2150
+ }
2151
+ }
2152
+ return { present: false, value: '' };
2153
+ }
2154
+ sameRequiredBinding(left, leftKeys, right, rightKeys) {
2155
+ const lhs = this.consistentBindingValue(left, leftKeys);
2156
+ const rhs = this.consistentBindingValue(right, rightKeys);
2157
+ return lhs.present && rhs.present && Boolean(lhs.value) && lhs.value === rhs.value;
2158
+ }
2159
+ consistentBindingValue(source, keys) {
2160
+ const binding = this.strictBoundString(source, keys);
2161
+ return binding.present && binding.valid
2162
+ ? { present: true, value: binding.value }
2163
+ : { present: false, value: '' };
2164
+ }
2165
+ strictBoundString(source, keys) {
2166
+ let value = null;
2167
+ for (const key of keys) {
2168
+ if (!Object.prototype.hasOwnProperty.call(source, key))
2169
+ continue;
2170
+ const raw = source[key];
2171
+ if (typeof raw !== 'string')
2172
+ return { present: true, valid: false, value: '' };
2173
+ const candidate = raw.trim();
2174
+ if (!candidate || (value !== null && candidate !== value)) {
2175
+ return { present: true, valid: false, value: '' };
2176
+ }
2177
+ value = candidate;
2178
+ }
2179
+ return { present: value !== null, valid: value !== null, value: value ?? '' };
2180
+ }
2181
+ onlyEmptyOptionalStrings(source, keys) {
2182
+ for (const key of keys) {
2183
+ if (!Object.prototype.hasOwnProperty.call(source, key))
2184
+ continue;
2185
+ const raw = source[key];
2186
+ if (typeof raw !== 'string' || raw.trim())
2187
+ return false;
2188
+ }
2189
+ return true;
2190
+ }
2191
+ inlineTargetMatchesCurrentInstance(head, inline) {
2192
+ const client = this.runtime.client;
2193
+ const explicitHeadDevice = this.strictTargetString(head, 'target_device_id');
2194
+ if (explicitHeadDevice.present && !explicitHeadDevice.valid)
2195
+ return false;
2196
+ const headDevice = explicitHeadDevice.present
2197
+ ? explicitHeadDevice
2198
+ : this.strictBoundString(head, ['device_id']);
2199
+ if (!headDevice.present || !headDevice.valid
2200
+ || headDevice.value !== String(client._deviceId ?? '').trim())
2201
+ return false;
2202
+ const explicitRowDevice = this.strictTargetString(inline, 'target_device_id');
2203
+ if (explicitRowDevice.present
2204
+ && (!explicitRowDevice.valid || explicitRowDevice.value !== headDevice.value))
2205
+ return false;
2206
+ if (!explicitRowDevice.present) {
2207
+ const rowDevice = this.strictBoundString(inline, ['device_id']);
2208
+ if (rowDevice.present && (!rowDevice.valid || rowDevice.value !== headDevice.value))
2209
+ return false;
2210
+ }
2211
+ const currentSlot = slotIsolationKey(String(client._slotId ?? ''));
2212
+ const explicitHeadSlot = this.strictTargetString(head, 'target_slot_id');
2213
+ if (explicitHeadSlot.present && !explicitHeadSlot.valid)
2214
+ return false;
2215
+ const headSlot = explicitHeadSlot.present
2216
+ ? explicitHeadSlot
2217
+ : this.strictBoundString(head, ['slot_id']);
2218
+ if (headSlot.present && (!headSlot.valid || slotIsolationKey(headSlot.value) !== currentSlot))
2219
+ return false;
2220
+ const explicitRowSlot = this.strictTargetString(inline, 'target_slot_id');
2221
+ if (explicitRowSlot.present && (!explicitRowSlot.valid
2222
+ || slotIsolationKey(explicitRowSlot.value) !== currentSlot
2223
+ || (headSlot.present
2224
+ && slotIsolationKey(explicitRowSlot.value) !== slotIsolationKey(headSlot.value))))
2225
+ return false;
2226
+ if (!explicitRowSlot.present) {
2227
+ const rowSlot = this.strictBoundString(inline, ['slot_id']);
2228
+ if (rowSlot.present && (!rowSlot.valid
2229
+ || slotIsolationKey(rowSlot.value) !== currentSlot
2230
+ || (headSlot.present
2231
+ && slotIsolationKey(rowSlot.value) !== slotIsolationKey(headSlot.value))))
2232
+ return false;
2233
+ }
2234
+ return true;
2235
+ }
2236
+ inlineV2Envelope(inline) {
2237
+ const raw = inline.envelope_json;
2238
+ if (typeof raw !== 'string' || !raw.trim())
2239
+ return null;
1285
2240
  try {
1286
- while (client._onlineUnreadHintQueue.size > 0) {
1287
- if (client.state !== ConnectionState.READY)
1288
- return;
1289
- if (client._sessionOptions?.background_sync === false)
2241
+ const envelope = JSON.parse(raw);
2242
+ return isJsonObject(envelope)
2243
+ ? envelope
2244
+ : null;
2245
+ }
2246
+ catch {
2247
+ return null;
2248
+ }
2249
+ }
2250
+ isInlineV2Row(inline) {
2251
+ return inline.version === 'v2'
2252
+ && typeof inline.envelope_json === 'string'
2253
+ && Boolean(inline.envelope_json.trim());
2254
+ }
2255
+ inlineRecipientMatchesCurrentInstance(envelope) {
2256
+ if (Object.prototype.hasOwnProperty.call(envelope, 'recipients'))
2257
+ return false;
2258
+ if (!isJsonObject(envelope.recipient))
2259
+ return false;
2260
+ const recipient = envelope.recipient;
2261
+ const client = this.runtime.client;
2262
+ if (typeof recipient.aid !== 'string' || typeof recipient.device_id !== 'string'
2263
+ || typeof client._aid !== 'string' || typeof client._deviceId !== 'string'
2264
+ || !recipient.aid || !client._aid || !client._deviceId
2265
+ || recipient.aid !== client._aid)
2266
+ return false;
2267
+ const hasDeliveryDevice = Object.prototype.hasOwnProperty.call(envelope, 'delivery_device_id');
2268
+ const deliveryDevice = envelope.delivery_device_id;
2269
+ if (hasDeliveryDevice
2270
+ && (typeof deliveryDevice !== 'string' || !deliveryDevice || deliveryDevice !== client._deviceId))
2271
+ return false;
2272
+ if (recipient.device_id)
2273
+ return recipient.device_id === client._deviceId;
2274
+ return hasDeliveryDevice;
2275
+ }
2276
+ inlineP2PBindingMatches(head, inline, seq, expectedInlineVersion) {
2277
+ if (typeof head.seq !== 'number' || !Number.isSafeInteger(head.seq) || head.seq <= 0 || head.seq !== seq
2278
+ || typeof inline.seq !== 'number' || !Number.isSafeInteger(inline.seq) || inline.seq <= 0 || inline.seq !== seq
2279
+ || inline.version !== expectedInlineVersion
2280
+ || !this.sameRequiredBinding(head, ['message_id'], inline, ['message_id'])
2281
+ || !this.sameRequiredBinding(head, ['from_aid', 'sender_aid', 'from'], inline, ['from_aid', 'sender_aid', 'from'])
2282
+ || !this.sameRequiredBinding(head, ['sender_device_id'], inline, ['sender_device_id'])
2283
+ || !this.inlineTargetMatchesCurrentInstance(head, inline))
2284
+ return false;
2285
+ const logicalTo = this.consistentBindingValue(head, ['to_aid', 'to']);
2286
+ if (!logicalTo.present || !logicalTo.value)
2287
+ return false;
2288
+ if (expectedInlineVersion === 'v1') {
2289
+ if (Object.prototype.hasOwnProperty.call(inline, 'envelope_json')
2290
+ || Object.prototype.hasOwnProperty.call(inline, 'payload')
2291
+ || !isJsonObject(inline.legacy_v1))
2292
+ return false;
2293
+ const legacy = inline.legacy_v1;
2294
+ const payload = legacy.payload;
2295
+ if (payload === undefined || payload === null)
2296
+ return false;
2297
+ if (isJsonObject(payload)) {
2298
+ const payloadType = String(payload.type ?? '').trim();
2299
+ if (payloadType === 'e2ee.encrypted' || payloadType === 'e2ee.group_encrypted')
2300
+ return false;
2301
+ }
2302
+ const legacyTo = this.strictBoundString(legacy, ['to']);
2303
+ return legacyTo.present && legacyTo.valid && legacyTo.value === logicalTo.value;
2304
+ }
2305
+ if (!this.isInlineV2Row(inline)
2306
+ || Object.prototype.hasOwnProperty.call(inline, 'legacy_v1')
2307
+ || Object.prototype.hasOwnProperty.call(inline, 'payload')
2308
+ || !this.onlyEmptyOptionalStrings(head, ['group_id', 'group_aid'])
2309
+ || !this.onlyEmptyOptionalStrings(inline, ['group_id', 'group_aid']))
2310
+ return false;
2311
+ const envelope = this.inlineV2Envelope(inline);
2312
+ if (!envelope
2313
+ || envelope.type !== 'e2ee.p2p_encrypted'
2314
+ || envelope.version !== 'v2'
2315
+ || !this.onlyEmptyOptionalStrings(envelope, ['group_id'])
2316
+ || !this.inlineRecipientMatchesCurrentInstance(envelope))
2317
+ return false;
2318
+ if (!isJsonObject(envelope.aad))
2319
+ return false;
2320
+ const aad = envelope.aad;
2321
+ if (!this.onlyEmptyOptionalStrings(aad, ['group_id']))
2322
+ return false;
2323
+ const aadMessageId = this.consistentBindingValue(aad, ['message_id']);
2324
+ const aadFrom = this.consistentBindingValue(aad, ['from']);
2325
+ const aadFromDevice = this.consistentBindingValue(aad, ['from_device']);
2326
+ const aadTo = this.consistentBindingValue(aad, ['to']);
2327
+ if (!aadMessageId.present || aadMessageId.value !== this.consistentBindingValue(inline, ['message_id']).value
2328
+ || !aadFrom.present || aadFrom.value !== this.consistentBindingValue(inline, ['from_aid', 'sender_aid', 'from']).value
2329
+ || !aadFromDevice.present || aadFromDevice.value !== this.consistentBindingValue(inline, ['sender_device_id']).value
2330
+ || !aadTo.present)
2331
+ return false;
2332
+ if (aadTo.value === logicalTo.value)
2333
+ return true;
2334
+ const recipient = envelope.recipient;
2335
+ return typeof recipient.role === 'string'
2336
+ && recipient.role.trim() === 'self_sync'
2337
+ && aadTo.value.length > 0
2338
+ && aadFrom.value === String(this.runtime.client._aid ?? '').trim()
2339
+ && logicalTo.value === String(this.runtime.client._aid ?? '').trim();
2340
+ }
2341
+ inlineGroupBindingMatches(head, inline, groupId, seq, expectedInlineVersion) {
2342
+ const headGroupId = this.strictBoundString(head, ['group_id']);
2343
+ const rowGroupId = this.strictBoundString(inline, ['group_id']);
2344
+ const headGroupAid = this.strictBoundString(head, ['group_aid']);
2345
+ const rowGroupAid = this.strictBoundString(inline, ['group_aid']);
2346
+ if (typeof head.seq !== 'number' || !Number.isSafeInteger(head.seq) || head.seq <= 0 || head.seq !== seq
2347
+ || typeof inline.seq !== 'number' || !Number.isSafeInteger(inline.seq) || inline.seq <= 0 || inline.seq !== seq
2348
+ || inline.version !== expectedInlineVersion
2349
+ || !headGroupId.present || !headGroupId.valid || headGroupId.value !== groupId
2350
+ || !rowGroupId.present || !rowGroupId.valid || rowGroupId.value !== groupId
2351
+ || (headGroupAid.present && !headGroupAid.valid)
2352
+ || (rowGroupAid.present && !rowGroupAid.valid)
2353
+ || !this.sameRequiredBinding(head, ['message_id'], inline, ['message_id'])
2354
+ || !this.sameRequiredBinding(head, ['sender_aid', 'from_aid', 'from'], inline, ['from_aid', 'sender_aid', 'from'])
2355
+ || !this.sameRequiredBinding(head, ['sender_device_id'], inline, ['sender_device_id'])
2356
+ || !this.inlineTargetMatchesCurrentInstance(head, inline))
2357
+ return false;
2358
+ if (expectedInlineVersion === 'v1') {
2359
+ if (Object.prototype.hasOwnProperty.call(inline, 'envelope_json')
2360
+ || Object.prototype.hasOwnProperty.call(inline, 'legacy_v1')
2361
+ || !Object.prototype.hasOwnProperty.call(inline, 'payload')
2362
+ || inline.payload === null
2363
+ || inline.payload === undefined)
2364
+ return false;
2365
+ if (isJsonObject(inline.payload)) {
2366
+ const payloadType = String(inline.payload.type ?? '').trim();
2367
+ if (payloadType === 'e2ee.encrypted' || payloadType === 'e2ee.group_encrypted')
2368
+ return false;
2369
+ }
2370
+ return true;
2371
+ }
2372
+ if (!this.isInlineV2Row(inline)
2373
+ || Object.prototype.hasOwnProperty.call(inline, 'legacy_v1')
2374
+ || Object.prototype.hasOwnProperty.call(inline, 'payload'))
2375
+ return false;
2376
+ const envelope = this.inlineV2Envelope(inline);
2377
+ const envelopeGroupId = envelope ? this.strictBoundString(envelope, ['group_id']) : null;
2378
+ if (!envelope
2379
+ || envelope.type !== 'e2ee.group_encrypted'
2380
+ || envelope.version !== 'v2'
2381
+ || (envelopeGroupId?.present && (!envelopeGroupId.valid || envelopeGroupId.value !== groupId))
2382
+ || !this.inlineRecipientMatchesCurrentInstance(envelope))
2383
+ return false;
2384
+ if (!isJsonObject(envelope.aad))
2385
+ return false;
2386
+ const aad = envelope.aad;
2387
+ return this.consistentBindingValue(aad, ['message_id']).value === this.consistentBindingValue(inline, ['message_id']).value
2388
+ && this.consistentBindingValue(aad, ['from']).value === this.consistentBindingValue(inline, ['from_aid', 'sender_aid', 'from']).value
2389
+ && this.consistentBindingValue(aad, ['from_device']).value === this.consistentBindingValue(inline, ['sender_device_id']).value
2390
+ && this.consistentBindingValue(aad, ['group_id']).value === groupId;
2391
+ }
2392
+ inlineWindowReady(ns, seq) {
2393
+ if (!ns || !Number.isSafeInteger(seq) || seq <= 0)
2394
+ return false;
2395
+ const expected = seq - 1;
2396
+ const state = this.syncState(ns);
2397
+ if (state.ack !== expected || state.tail !== expected || state.head !== expected)
2398
+ return false;
2399
+ const client = this.runtime.client;
2400
+ // 入口会先登记当前 Push 的 Head,因此 maxSeen==seq 仍是合法的
2401
+ // 单条 inline 快路径;只有已经观察到更高序号时才退回同步路径。
2402
+ if (Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? expected) > seq)
2403
+ return false;
2404
+ const pending = client._pendingOrderedMsgs.get(ns);
2405
+ return !pending || pending.size === 0;
2406
+ }
2407
+ decodedInlineP2PMatches(head, decoded, seq) {
2408
+ const decodedMessageId = this.consistentBindingValue(decoded, ['message_id']);
2409
+ const headMessageId = this.consistentBindingValue(head, ['message_id']);
2410
+ const decodedFrom = this.consistentBindingValue(decoded, ['from', 'from_aid', 'sender_aid']);
2411
+ const headFrom = this.consistentBindingValue(head, ['from_aid', 'sender_aid', 'from']);
2412
+ const decodedTo = this.consistentBindingValue(decoded, ['to', 'to_aid']);
2413
+ const headTo = this.consistentBindingValue(head, ['to_aid', 'to']);
2414
+ return typeof decoded.seq === 'number' && Number.isSafeInteger(decoded.seq) && decoded.seq === seq
2415
+ && decodedMessageId.present && headMessageId.present && decodedMessageId.value === headMessageId.value
2416
+ && decodedFrom.present && headFrom.present && decodedFrom.value === headFrom.value
2417
+ && decodedTo.present && headTo.present && decodedTo.value === headTo.value;
2418
+ }
2419
+ decodedInlineGroupMatches(head, decoded, groupId, seq) {
2420
+ const decodedMessageId = this.consistentBindingValue(decoded, ['message_id']);
2421
+ const headMessageId = this.consistentBindingValue(head, ['message_id']);
2422
+ const decodedFrom = this.consistentBindingValue(decoded, ['from', 'from_aid', 'sender_aid']);
2423
+ const headFrom = this.consistentBindingValue(head, ['sender_aid', 'from_aid', 'from']);
2424
+ const decodedGroupId = this.consistentBindingValue(decoded, ['group_id']);
2425
+ return typeof decoded.seq === 'number' && Number.isSafeInteger(decoded.seq) && decoded.seq === seq
2426
+ && decodedMessageId.present && headMessageId.present && decodedMessageId.value === headMessageId.value
2427
+ && decodedFrom.present && headFrom.present && decodedFrom.value === headFrom.value
2428
+ && decodedGroupId.present && decodedGroupId.value === groupId;
2429
+ }
2430
+ async commitInlineMessage(ns, seq, event, payload, generation) {
2431
+ const client = this.runtime.client;
2432
+ const tracker = client._seqTracker;
2433
+ const snapshot = typeof tracker.snapshotNamespace === 'function'
2434
+ ? tracker.snapshotNamespace(ns)
2435
+ : null;
2436
+ try {
2437
+ const published = await this.publishPulledMessage(event, ns, seq, payload);
2438
+ if (!this.isInlineGenerationCurrent(generation))
2439
+ return false;
2440
+ if (!published)
2441
+ return false;
2442
+ const needsPull = Boolean(tracker.onMessageSeq(ns, seq));
2443
+ const committed = this.syncState(ns);
2444
+ if (needsPull || committed.ack !== seq || committed.tail !== seq || committed.head !== seq) {
2445
+ throw new Error('inline realtime cursor did not collapse after consume');
2446
+ }
2447
+ if (!this.isInlineGenerationCurrent(generation))
2448
+ return false;
2449
+ await client._commitSeqTrackerState(ns);
2450
+ if (!this.isInlineGenerationCurrent(generation))
2451
+ return false;
2452
+ return true;
2453
+ }
2454
+ catch (exc) {
2455
+ if (this.isInlineGenerationCurrent(generation) && typeof tracker.restoreNamespaceSnapshot === 'function') {
2456
+ tracker.restoreNamespaceSnapshot(ns, snapshot);
2457
+ }
2458
+ client._clientLog.debug(`inline realtime consume rolled back: ns=${ns} seq=${seq} err=${formatDeliveryError(exc)}`);
2459
+ return false;
2460
+ }
2461
+ }
2462
+ async tryConsumeInlineP2P(head, inline, ns, seq, expectedInlineVersion, generation) {
2463
+ if (!this.isInlineGenerationCurrent(generation)
2464
+ || !this.inlineP2PBindingMatches(head, inline, seq, expectedInlineVersion) || !this.inlineWindowReady(ns, seq))
2465
+ return false;
2466
+ const client = this.runtime.client;
2467
+ try {
2468
+ const result = await client._rpcPipeline.tryRunInlineRealtime(ns, seq, async () => {
2469
+ if (!this.isInlineGenerationCurrent(generation)
2470
+ || !this.inlineWindowReady(ns, seq)
2471
+ || !this.inlineP2PBindingMatches(head, inline, seq, expectedInlineVersion)
2472
+ || !this.reservePendingP2PInlineAck(ns, seq, inline, generation))
2473
+ return false;
2474
+ let committed = false;
2475
+ try {
2476
+ const decoded = await client._v2E2EE.decodeInlineP2PMessage(inline);
2477
+ if (!this.isInlineGenerationCurrent(generation)
2478
+ || !decoded || !this.decodedInlineP2PMatches(head, decoded, seq)
2479
+ || !this.inlineWindowReady(ns, seq))
2480
+ return false;
2481
+ const appEvent = this.p2pAppEventForMessage(decoded);
2482
+ if (!await this.commitInlineMessage(ns, seq, appEvent.event, appEvent.payload, generation))
2483
+ return false;
2484
+ if (!this.isInlineGenerationCurrent(generation))
2485
+ return false;
2486
+ committed = this.markPendingP2PInlineAckCommitted(ns, seq, generation);
2487
+ return committed;
2488
+ }
2489
+ finally {
2490
+ if (!committed && this.isInlineGenerationCurrent(generation))
2491
+ this.dropPendingP2PInlineAck(ns, seq, generation);
2492
+ }
2493
+ });
2494
+ const consumed = this.isInlineGenerationCurrent(generation) && result.acquired === true && result.value === true;
2495
+ return consumed;
2496
+ }
2497
+ catch (exc) {
2498
+ client._clientLog.debug(`inline P2P consume failed: seq=${seq} err=${formatDeliveryError(exc)}`);
2499
+ return false;
2500
+ }
2501
+ }
2502
+ async tryConsumeInlineGroup(head, inline, groupId, ns, seq, expectedInlineVersion, generation) {
2503
+ if (!this.isInlineGenerationCurrent(generation)
2504
+ || !this.inlineGroupBindingMatches(head, inline, groupId, seq, expectedInlineVersion) || !this.inlineWindowReady(ns, seq))
2505
+ return false;
2506
+ const client = this.runtime.client;
2507
+ const groupAid = typeof head.group_aid === 'string' && head.group_aid.trim()
2508
+ ? head.group_aid.trim()
2509
+ : groupId;
2510
+ try {
2511
+ const result = await client._rpcPipeline.tryRunInlineRealtime(ns, seq, async () => {
2512
+ if (!this.isInlineGenerationCurrent(generation)
2513
+ || !this.inlineWindowReady(ns, seq)
2514
+ || !this.inlineGroupBindingMatches(head, inline, groupId, seq, expectedInlineVersion)
2515
+ || !this.reservePendingGroupInlineAck(ns, seq, inline, groupAid, groupId, generation))
2516
+ return false;
2517
+ let committed = false;
2518
+ try {
2519
+ const decoded = await client._v2E2EE.decodeInlineGroupMessage(inline, groupId, groupAid);
2520
+ if (!this.isInlineGenerationCurrent(generation)
2521
+ || !decoded || !this.decodedInlineGroupMatches(head, decoded, groupId, seq)
2522
+ || !this.inlineWindowReady(ns, seq))
2523
+ return false;
2524
+ if (!await this.commitInlineMessage(ns, seq, 'group.message_created', decoded, generation))
2525
+ return false;
2526
+ if (!this.isInlineGenerationCurrent(generation))
2527
+ return false;
2528
+ committed = this.markPendingGroupInlineAckCommitted(ns, seq, generation);
2529
+ return committed;
2530
+ }
2531
+ finally {
2532
+ if (!committed && this.isInlineGenerationCurrent(generation))
2533
+ this.dropPendingGroupInlineAck(ns, seq, generation);
2534
+ }
2535
+ });
2536
+ const consumed = this.isInlineGenerationCurrent(generation) && result.acquired === true && result.value === true;
2537
+ return consumed;
2538
+ }
2539
+ catch (exc) {
2540
+ client._clientLog.debug(`inline Group consume failed: group=${groupId} seq=${seq} err=${formatDeliveryError(exc)}`);
2541
+ return false;
2542
+ }
2543
+ }
2544
+ hasOutstandingRealtimeGap(ns) {
2545
+ const state = this.syncState(ns);
2546
+ // Tail 已建立可靠后缀窗口;后台续拉只针对尚未接上窗口的真实缺口。
2547
+ // maxSeen 可能只是 Tail 期间到达的通知,不能单独据此启动全量补洞。
2548
+ return state.ack < state.tail - 1;
2549
+ }
2550
+ scheduleP2PRealtimeFullGapFill(ns) {
2551
+ const client = this.runtime.client;
2552
+ const currentNs = client._aid ? `p2p:${client._aid}` : '';
2553
+ if (client._sessionOptions?.background_sync === false
2554
+ || client.state !== ConnectionState.READY || client._closing
2555
+ || currentNs !== ns || !this.hasOutstandingRealtimeGap(ns))
2556
+ return;
2557
+ const scheduleKey = `p2p_realtime_full_gap:${ns}`;
2558
+ if (client._gapFillDone.has(scheduleKey))
2559
+ return;
2560
+ client._gapFillDone.add(scheduleKey);
2561
+ client._safeAsync(Promise.resolve().then(async () => {
2562
+ try {
2563
+ const liveNs = client._aid ? `p2p:${client._aid}` : '';
2564
+ if (client._sessionOptions?.background_sync === false
2565
+ || client.state !== ConnectionState.READY || client._closing
2566
+ || liveNs !== ns || !this.hasOutstandingRealtimeGap(ns))
1290
2567
  return;
1291
- const groupId = client._onlineUnreadHintQueue.keys().next().value;
1292
- if (!groupId)
2568
+ await this.fillP2pGap();
2569
+ }
2570
+ finally {
2571
+ client._gapFillDone.delete(scheduleKey);
2572
+ }
2573
+ }));
2574
+ }
2575
+ scheduleGroupRealtimeFullGapFill(groupId, ns) {
2576
+ const client = this.runtime.client;
2577
+ groupId = String(groupId ?? '').trim();
2578
+ if (!groupId || client._sessionOptions?.background_sync === false
2579
+ || client.state !== ConnectionState.READY || client._closing
2580
+ || !this.hasOutstandingRealtimeGap(ns))
2581
+ return;
2582
+ const scheduleKey = `group_realtime_full_gap:${ns}`;
2583
+ if (client._gapFillDone.has(scheduleKey))
2584
+ return;
2585
+ client._gapFillDone.add(scheduleKey);
2586
+ client._safeAsync(Promise.resolve().then(async () => {
2587
+ try {
2588
+ if (client._sessionOptions?.background_sync === false
2589
+ || client.state !== ConnectionState.READY || client._closing
2590
+ || !this.hasOutstandingRealtimeGap(ns))
1293
2591
  return;
1294
- const payload = client._onlineUnreadHintQueue.get(groupId);
1295
- client._onlineUnreadHintQueue.delete(groupId);
1296
- if (!payload)
1297
- continue;
1298
- await this.onRawGroupV2MessageCreated({ ...payload, _online_hint_drained: true });
1299
- const intervalMs = Math.max(0, Number(client._onlineUnreadHintIntervalMs ?? 50) || 0);
1300
- if (intervalMs > 0 && client._onlineUnreadHintQueue.size > 0) {
1301
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
1302
- }
2592
+ await this.fillGroupGap(groupId);
1303
2593
  }
2594
+ finally {
2595
+ client._gapFillDone.delete(scheduleKey);
2596
+ }
2597
+ }));
2598
+ }
2599
+ async runP2PRealtimeSync(ns, pushSeq, pageLimit = 50, forceTail = false) {
2600
+ const client = this.runtime.client;
2601
+ const before = this.syncState(ns);
2602
+ const order = forceTail ? ['tail'] : typeof client._seqTracker.planRealtimeSync === 'function'
2603
+ ? client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit)
2604
+ : (() => {
2605
+ const gap = pushSeq - before.ack;
2606
+ if (gap <= 0)
2607
+ return before.ack < before.tail - 1 ? ['forward'] : [];
2608
+ return gap <= pageLimit * 2 ? ['forward'] : ['tail'];
2609
+ })();
2610
+ // Planner 依据 S-A 选路;H 已覆盖 S 也不代表 A 已连续提交,仍需前台 Forward。
2611
+ const headForward = order[0] === 'forward' && pushSeq > before.ack;
2612
+ let result = { messages: [], raw_count: 0 };
2613
+ let tailCompleted = false;
2614
+ const run = async (tail, background, maxPages, forceForward = false, bypassCoveredHead = false) => {
2615
+ const live = this.syncState(ns);
2616
+ if (tail && !bypassCoveredHead && pushSeq <= live.head)
2617
+ return { messages: [], raw_count: 0 };
2618
+ if (!tail && !forceForward && live.ack >= live.tail - 1)
2619
+ return { messages: [], raw_count: 0 };
2620
+ const request = { ...(tail ? { window_mode: 'tail' } : {}), after_seq: live.ack, limit: pageLimit };
2621
+ const key = client._rpcPipeline.pullGateKeyForCall('message.pull', request);
2622
+ return await client._rpcPipeline.runPullSerialized(key, async () => {
2623
+ if (tail) {
2624
+ const tailResult = await client._v2E2EE.pullV2TailInternal({
2625
+ window_mode: 'tail', after_seq: live.ack, limit: pageLimit,
2626
+ }, true);
2627
+ tailCompleted = true;
2628
+ return tailResult;
2629
+ }
2630
+ const messages = await client._pullV2(live.ack, pageLimit, {
2631
+ gateLocked: true,
2632
+ ...(maxPages ? { maxPages } : {}),
2633
+ });
2634
+ return { messages, raw_count: messages.length };
2635
+ }, background);
2636
+ };
2637
+ if (forceTail || order[0] === 'tail')
2638
+ result = await run(true, false, undefined, false, forceTail);
2639
+ const state = this.syncState(ns);
2640
+ // Tail 的连续性证明只覆盖其返回的 head。Tail 进行期间到达的新
2641
+ // Push 不能再次进入 realtime planner(那会再做一次 Tail);以已经
2642
+ // 落盘的 ack 为起点,至多补一页普通 Forward 即可恢复该小窗口。
2643
+ const tailMissedDuringPull = tailCompleted
2644
+ && Number(client._seqTracker.getMaxSeenSeq(ns) || 0) > state.head;
2645
+ const pendingThroughSeq = tailMissedDuringPull
2646
+ ? Number(client._seqTracker.getMaxSeenSeq(ns) || 0)
2647
+ : 0;
2648
+ const tailForward = tailCompleted && (state.ack < state.tail - 1 || tailMissedDuringPull);
2649
+ const backgroundWindowForward = order[0] === 'forward' && !headForward;
2650
+ if (headForward || tailForward
2651
+ || (client._sessionOptions?.background_sync !== false && backgroundWindowForward)) {
2652
+ result = await run(false, !(headForward || tailForward), headForward ? 2 : order[0] === 'tail' ? 1 : undefined, headForward || tailMissedDuringPull);
2653
+ if (pendingThroughSeq > 0)
2654
+ this.consumePendingPull(ns, pendingThroughSeq);
2655
+ }
2656
+ const finalState = this.syncState(ns);
2657
+ if (tailCompleted && finalState.ack < finalState.tail - 1) {
2658
+ this.scheduleP2PRealtimeFullGapFill(ns);
2659
+ }
2660
+ if (tailCompleted)
2661
+ this.markRealtimeTailCompleted(result);
2662
+ return result;
2663
+ }
2664
+ async runGroupRealtimeSync(groupId, ns, pushSeq, pageLimit = 50, forceTail = false) {
2665
+ const client = this.runtime.client;
2666
+ const before = this.syncState(ns);
2667
+ const order = forceTail ? ['tail'] : typeof client._seqTracker.planRealtimeSync === 'function'
2668
+ ? client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit)
2669
+ : (() => {
2670
+ const gap = pushSeq - before.ack;
2671
+ if (gap <= 0)
2672
+ return before.ack < before.tail - 1 ? ['forward'] : [];
2673
+ return gap <= pageLimit * 2 ? ['forward'] : ['tail'];
2674
+ })();
2675
+ // Planner 依据 S-A 选路;H 已覆盖 S 也不代表 A 已连续提交,仍需前台 Forward。
2676
+ const headForward = order[0] === 'forward' && pushSeq > before.ack;
2677
+ let result = { messages: [], raw_count: 0 };
2678
+ let tailCompleted = false;
2679
+ const run = async (tail, background, maxPages, forceForward = false, bypassCoveredHead = false) => {
2680
+ const live = this.syncState(ns);
2681
+ if (tail && !bypassCoveredHead && pushSeq <= live.head)
2682
+ return { messages: [], raw_count: 0 };
2683
+ if (!tail && !forceForward && live.ack >= live.tail - 1)
2684
+ return { messages: [], raw_count: 0 };
2685
+ const request = { group_id: groupId, ...(tail ? { window_mode: 'tail' } : {}), after_seq: live.ack, limit: pageLimit };
2686
+ const key = client._rpcPipeline.pullGateKeyForCall('group.pull', request);
2687
+ return await client._rpcPipeline.runPullSerialized(key, async () => {
2688
+ if (tail) {
2689
+ const tailResult = await client._v2E2EE.pullGroupV2TailInternal({
2690
+ group_id: groupId, window_mode: 'tail', after_seq: live.ack, limit: pageLimit,
2691
+ }, true);
2692
+ tailCompleted = true;
2693
+ return tailResult;
2694
+ }
2695
+ const messages = await client._pullGroupV2(groupId, live.ack, pageLimit, {
2696
+ gateLocked: true,
2697
+ ...(maxPages ? { maxPages } : {}),
2698
+ });
2699
+ return { messages, raw_count: messages.length };
2700
+ }, background);
2701
+ };
2702
+ if (forceTail || order[0] === 'tail')
2703
+ result = await run(true, false, undefined, false, forceTail);
2704
+ const state = this.syncState(ns);
2705
+ // 与 P2P 相同:真实完成 Tail 后,至多用一页 Forward 收敛 Tail
2706
+ // 期间新到达的序号;绝不重新规划或再执行 Tail。
2707
+ const tailMissedDuringPull = tailCompleted
2708
+ && Number(client._seqTracker.getMaxSeenSeq(ns) || 0) > state.head;
2709
+ const pendingThroughSeq = tailMissedDuringPull
2710
+ ? Number(client._seqTracker.getMaxSeenSeq(ns) || 0)
2711
+ : 0;
2712
+ const tailForward = tailCompleted && (state.ack < state.tail - 1 || tailMissedDuringPull);
2713
+ const backgroundWindowForward = order[0] === 'forward' && !headForward;
2714
+ if (headForward || tailForward
2715
+ || (client._sessionOptions?.background_sync !== false && backgroundWindowForward)) {
2716
+ result = await run(false, !(headForward || tailForward), headForward ? 2 : order[0] === 'tail' ? 1 : undefined, headForward || tailMissedDuringPull);
2717
+ if (pendingThroughSeq > 0)
2718
+ this.consumePendingPull(ns, pendingThroughSeq);
2719
+ }
2720
+ const finalState = this.syncState(ns);
2721
+ if (tailCompleted && finalState.ack < finalState.tail - 1) {
2722
+ this.scheduleGroupRealtimeFullGapFill(groupId, ns);
2723
+ }
2724
+ if (tailCompleted)
2725
+ this.markRealtimeTailCompleted(result);
2726
+ return result;
2727
+ }
2728
+ async recoverInvalidP2PRealtimeHead(ns, rawSeq) {
2729
+ const client = this.runtime.client;
2730
+ client._clientLog.warn(`V2 P2P inline Head has invalid seq; forcing authoritative Tail: seq=${String(rawSeq)}`);
2731
+ if (!this.beginRealtimeSync(ns))
2732
+ return;
2733
+ try {
2734
+ await this.runP2PRealtimeSync(ns, this.syncState(ns).head, 50, true);
1304
2735
  }
1305
2736
  catch (exc) {
1306
- client._clientLog.debug(`online unread hint drain failed: ${formatDeliveryError(exc)}`);
2737
+ client._clientLog.warn(`V2 P2P invalid-seq Tail failed: ${formatDeliveryError(exc)}`);
1307
2738
  }
1308
2739
  finally {
1309
- this.runtime.delivery.setOnlineUnreadHintDrainActive(false);
2740
+ this.endRealtimeSync(ns);
1310
2741
  }
1311
2742
  }
1312
- async onRawGroupV2MessageCreated(data) {
2743
+ async recoverInvalidGroupRealtimeHead(groupId, ns, rawSeq) {
1313
2744
  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}`);
2745
+ client._clientLog.warn(`V2 Group inline Head has invalid seq; forcing authoritative Tail: group=${groupId} seq=${String(rawSeq)}`);
2746
+ if (!this.beginRealtimeSync(ns))
1317
2747
  return;
2748
+ try {
2749
+ await this.runGroupRealtimeSync(groupId, ns, this.syncState(ns).head, 50, true);
1318
2750
  }
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;
2751
+ catch (exc) {
2752
+ client._clientLog.warn(`V2 Group invalid-seq Tail failed: group=${groupId} err=${formatDeliveryError(exc)}`);
1327
2753
  }
1328
- if (!client._v2Session) {
1329
- client._clientLog.debug('_onRawGroupV2MessageCreated skipped: V2 session not initialized');
2754
+ finally {
2755
+ this.endRealtimeSync(ns);
2756
+ }
2757
+ }
2758
+ async onRawGroupV2MessageCreated(data) {
2759
+ const client = this.runtime.client;
2760
+ const row = isJsonObject(data) ? data : null;
2761
+ if (!row)
2762
+ return;
2763
+ const groupId = typeof row.group_id === 'string' ? row.group_id.trim() : '';
2764
+ if (!groupId)
1330
2765
  return;
2766
+ const seq = typeof row.seq === 'number' ? row.seq : 0;
2767
+ const eventKind = String(row.kind ?? '').trim();
2768
+ const ns = this.resolvePendingGroupInlineAckNamespace(`group:${groupId}`);
2769
+ if (client._v2Session && eventKind !== 'group.online_unread_hint'
2770
+ && Number.isSafeInteger(seq) && seq > 0) {
2771
+ // 必须在进入 namespace 队列前登记,才能让在途 Tail 看见后来 Push。
2772
+ client._seqTracker.updateMaxSeen(ns, seq);
1331
2773
  }
2774
+ const hasInlineObject = isJsonObject(row.inline_message)
2775
+ && eventKind !== 'group.online_unread_hint';
2776
+ const inlineGeneration = hasInlineObject ? this.captureInlineGeneration() : undefined;
2777
+ const operation = () => this.processGroupV2MessageCreated(data, 'v2', inlineGeneration);
2778
+ if (hasInlineObject)
2779
+ await this.runPushProcessSerialized(ns, operation, inlineGeneration);
2780
+ else
2781
+ await operation();
2782
+ }
2783
+ async processGroupV2MessageCreated(data, expectedInlineVersion, queuedInlineGeneration) {
2784
+ if (queuedInlineGeneration !== undefined && !this.isInlineGenerationCurrent(queuedInlineGeneration))
2785
+ return;
2786
+ const client = this.runtime.client;
2787
+ if (!isJsonObject(data) || !client._v2Session)
2788
+ return;
2789
+ const groupId = typeof data.group_id === 'string' ? data.group_id.trim() : '';
2790
+ const seq = positiveSafeSequenceHint(data.seq);
2791
+ const strictSeq = typeof data.seq === 'number' && Number.isSafeInteger(data.seq) && data.seq > 0;
2792
+ if (!groupId)
2793
+ return;
1332
2794
  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`);
2795
+ // Hint 是恢复信号而不是消息 Head:不得消费 inline、推进水位或直接 ACK。
2796
+ if (eventKind === 'group.online_unread_hint') {
2797
+ if (client._sessionOptions?.background_sync === false)
1336
2798
  return;
1337
- }
1338
2799
  this.enqueueOnlineUnreadHint(data);
1339
2800
  return;
1340
2801
  }
1341
- try {
1342
- const ns = `group:${groupId}`;
2802
+ const ns = this.resolvePendingGroupInlineAckNamespace(`group:${groupId}`);
2803
+ const inlinePresent = Object.prototype.hasOwnProperty.call(data, 'inline_message');
2804
+ if (seq <= 0) {
2805
+ if (inlinePresent)
2806
+ await this.recoverInvalidGroupRealtimeHead(groupId, ns, data.seq);
2807
+ return;
2808
+ }
2809
+ const inline = this.inlineMessage(data);
2810
+ const inlineMatches = inline !== null
2811
+ && this.inlineGroupBindingMatches(data, inline, groupId, seq, expectedInlineVersion);
2812
+ const pendingInline = this.pendingGroupInlineAcks?.get(ns);
2813
+ const pendingConflict = inline !== null && pendingInline !== undefined && pendingInline.seq !== seq;
2814
+ const inlineGeneration = inlineMatches ? (queuedInlineGeneration ?? this.inlineGeneration) : -1;
2815
+ if (inlineMatches && !pendingConflict
2816
+ && await this.tryConsumeInlineGroup(data, inline, groupId, ns, seq, expectedInlineVersion, inlineGeneration)) {
2817
+ const pending = this.pendingGroupInlineAcks?.get(ns);
2818
+ if (pending)
2819
+ this.retryPendingGroupInlineAck(ns, pending.seq, 'inline Group ack');
2820
+ return;
2821
+ }
2822
+ if (inlineMatches && !this.isInlineGenerationCurrent(inlineGeneration))
2823
+ return;
2824
+ const before = this.syncState(ns);
2825
+ // 无正文的旧服务端 Head 可把 JSON-safe 数字字符串作为恢复 hint;
2826
+ // inline 绑定仍要求原始 seq 是严格 number,避免类型归一化后消费正文。
2827
+ if (strictSeq || !inlinePresent)
1343
2828
  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;
2829
+ if (seq <= before.ack && before.ack >= before.tail - 1) {
2830
+ const canonicalNs = this.resolvePendingGroupInlineAckNamespace(ns);
2831
+ const pending = this.pendingGroupInlineAcks?.get(canonicalNs);
2832
+ if (pending && seq >= pending.seq) {
2833
+ this.retryPendingGroupInlineAck(canonicalNs, pending.seq, 'covered inline Group ack', seq);
1349
2834
  }
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;
1355
- }
1356
- client._gapFillDone.add(dedupKey);
1357
- 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}`);
1361
- }
1362
- finally {
1363
- client._gapFillDone.delete(dedupKey);
2835
+ else if (!inlinePresent || inlineMatches) {
2836
+ this.fireGroupV2Ack(groupId, ns, seq, 'group v2 covered push ack', inlineMatches ? inlineGeneration : null);
1364
2837
  }
2838
+ return;
2839
+ }
2840
+ const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
2841
+ if (pullActive || !this.beginRealtimeSync(ns)) {
2842
+ this.recordPendingPull(ns, seq);
2843
+ client._clientLog?.debug(`V2 group push queued behind in-flight realtime sync: ns=${ns} seq=${seq}`);
2844
+ return;
2845
+ }
2846
+ try {
2847
+ // namespace 串行区必须覆盖 Tail 与其后至多一次 Forward 的完整周期。
2848
+ await this.runGroupRealtimeSync(groupId, ns, seq, 50, false);
1365
2849
  }
1366
2850
  catch (exc) {
1367
- client._clientLog.warn(`_onRawGroupV2MessageCreated pull failed group=${groupId}: ${String(exc)}`);
2851
+ client._clientLog.warn(`V2 group push auto-sync failed: group=${groupId} err=${formatDeliveryError(exc)}`);
1368
2852
  }
1369
2853
  finally {
1370
- client._clientLog.debug(`_onRawGroupV2MessageCreated exit: elapsed=${Date.now() - tStart}ms`);
2854
+ this.endRealtimeSync(ns);
2855
+ this.schedulePendingPullIfNeeded(ns, 'push-auto-pull-complete');
1371
2856
  }
1372
2857
  }
1373
2858
  async onV2PushNotification(data) {
1374
2859
  const client = this.runtime.client;
2860
+ const ns = client._aid ? `p2p:${client._aid}` : '';
2861
+ if (!ns)
2862
+ return;
2863
+ if (isJsonObject(data) && String(data.kind ?? '').trim() === 'message.online_unread_hint') {
2864
+ if (client._sessionOptions?.background_sync === false)
2865
+ return;
2866
+ this.enqueueOnlineUnreadP2pHint(data);
2867
+ return;
2868
+ }
2869
+ const pushSeq = isJsonObject(data) && typeof data.seq === 'number' ? data.seq : 0;
2870
+ if (client._v2Session && Number.isSafeInteger(pushSeq) && pushSeq > 0) {
2871
+ // 与 Group 相同,在排队前暴露更高 Head 给当前在途 Tail。
2872
+ client._seqTracker.updateMaxSeen(ns, pushSeq);
2873
+ }
2874
+ const hasInlineObject = isJsonObject(data)
2875
+ && isJsonObject(data.inline_message);
2876
+ const inlineGeneration = hasInlineObject ? this.captureInlineGeneration() : undefined;
2877
+ const operation = () => this.processV2P2PNotification(data, 'v2', inlineGeneration);
2878
+ if (hasInlineObject)
2879
+ await this.runPushProcessSerialized(ns, operation, inlineGeneration);
2880
+ else
2881
+ await operation();
2882
+ }
2883
+ async processV2P2PNotification(data, expectedInlineVersion, queuedInlineGeneration) {
2884
+ if (queuedInlineGeneration !== undefined && !this.isInlineGenerationCurrent(queuedInlineGeneration))
2885
+ return;
2886
+ const client = this.runtime.client;
1375
2887
  if (!client._v2Session)
1376
2888
  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;
2889
+ const pushSeq = isJsonObject(data) ? positiveSafeSequenceHint(data.seq) : 0;
2890
+ const strictSeq = isJsonObject(data)
2891
+ && typeof data.seq === 'number' && Number.isSafeInteger(data.seq) && data.seq > 0;
1382
2892
  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`);
1389
- return;
1390
- }
1391
- contigBefore = client._repairPushContiguousBound(ns, pushSeq, hasPayload, '_raw.peer.v2.message_received');
2893
+ if (!ns)
2894
+ return;
2895
+ const inlinePresent = isJsonObject(data) && Object.prototype.hasOwnProperty.call(data, 'inline_message');
2896
+ if (pushSeq <= 0) {
2897
+ if (inlinePresent)
2898
+ await this.recoverInvalidP2PRealtimeHead(ns, isJsonObject(data) ? data.seq : undefined);
2899
+ return;
1392
2900
  }
1393
- if (hasPayload && pushSeq > 0 && ns) {
1394
- 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
- }
2901
+ const inline = isJsonObject(data) ? this.inlineMessage(data) : null;
2902
+ const inlineMatches = inline !== null
2903
+ && this.inlineP2PBindingMatches(data, inline, pushSeq, expectedInlineVersion);
2904
+ const pendingInline = this.pendingP2PInlineAcks?.get(ns);
2905
+ const pendingConflict = inline !== null && pendingInline !== undefined && pendingInline.seq !== pushSeq;
2906
+ const inlineGeneration = inlineMatches ? (queuedInlineGeneration ?? this.inlineGeneration) : -1;
2907
+ if (inlineMatches && !pendingConflict
2908
+ && await this.tryConsumeInlineP2P(data, inline, ns, pushSeq, expectedInlineVersion, inlineGeneration)) {
2909
+ this.retryPendingP2PInlineAck(ns, pushSeq, 'inline P2P ack');
2910
+ return;
2911
+ }
2912
+ if (inlineMatches && !this.isInlineGenerationCurrent(inlineGeneration))
2913
+ return;
2914
+ const before = this.syncState(ns);
2915
+ if (strictSeq || !inlinePresent)
2916
+ client._seqTracker.updateMaxSeen(ns, pushSeq);
2917
+ if (pushSeq <= before.ack && before.ack >= before.tail - 1) {
2918
+ const pending = this.pendingP2PInlineAcks?.get(ns);
2919
+ if (pending && pushSeq >= pending.seq) {
2920
+ this.retryPendingP2PInlineAck(ns, pending.seq, 'covered inline P2P ack', pushSeq);
1418
2921
  }
1419
- catch (exc) {
1420
- client._clientLog.debug(`_onV2PushNotification: push payload 解密失败, fallback to pull: ${exc}`);
2922
+ else if (inlineMatches) {
2923
+ this.fireP2PV2Ack(pushSeq, 'P2P inline covered push ack', inlineGeneration);
1421
2924
  }
2925
+ return;
1422
2926
  }
1423
- if (pushSeq > 0 && ns) {
1424
- client._clientLog.debug(`_onV2PushNotification: 纯通知 push_seq=${pushSeq} > contiguous_seq=${contigBefore}, 触发 pull(after_seq=${contigBefore})`);
1425
- }
1426
- if (client._v2PullInflight) {
1427
- this.runtime.delivery.setV2PullPending(true);
2927
+ const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
2928
+ if (pullActive || !this.beginRealtimeSync(ns)) {
2929
+ this.recordPendingPull(ns, pushSeq);
2930
+ client._clientLog?.debug(`V2 P2P push queued behind in-flight realtime sync: ns=${ns} seq=${pushSeq}`);
1428
2931
  return;
1429
2932
  }
1430
- this.runtime.delivery.setV2PullInflight(true);
1431
- const dedupKey = `p2p_pull:${ns}`;
1432
- client._gapFillDone.add(dedupKey);
1433
2933
  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);
2934
+ await this.runP2PRealtimeSync(ns, pushSeq, 50, false);
1440
2935
  }
1441
2936
  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}`);
2937
+ client._clientLog.warn(`V2 P2P push auto-sync failed: ${formatDeliveryError(exc)}`);
1444
2938
  }
1445
2939
  finally {
1446
- this.runtime.delivery.setV2PullInflight(false);
1447
- client._gapFillDone.delete(dedupKey);
2940
+ this.endRealtimeSync(ns);
2941
+ this.schedulePendingPullIfNeeded(ns, 'push-auto-pull-complete');
1448
2942
  }
1449
2943
  }
2944
+ enqueueOnlineUnreadP2pHint(data) {
2945
+ const client = this.runtime.client;
2946
+ if (!client._aid || !client._v2Session)
2947
+ return;
2948
+ const ns = `p2p:${client._aid}`;
2949
+ client._safeAsync(this.runP2PForwardRecovery(ns, 50, true).catch((exc) => {
2950
+ client._clientLog?.debug(`P2P online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
2951
+ }));
2952
+ }
1450
2953
  async restoreSeqTrackerState() {
1451
2954
  const client = this.runtime.client;
1452
2955
  if (!client._aid)
@@ -1458,6 +2961,17 @@ export class MessageDeliveryEngine {
1458
2961
  const deviceId = client._deviceId;
1459
2962
  const slotId = client._slotId;
1460
2963
  try {
2964
+ const loadSync = client._tokenStore.loadAllSyncStates?.bind(client._tokenStore);
2965
+ if (typeof loadSync === 'function' && typeof client._seqTracker.restoreSyncState === 'function') {
2966
+ let state = await loadSync(aid, deviceId, slotId);
2967
+ if (client._seqTrackerContext !== context)
2968
+ return;
2969
+ if (state && typeof state === 'object' && Object.keys(state).length > 0) {
2970
+ state = await this.migrateSeqStateGroupIds(state);
2971
+ client._seqTracker.restoreSyncState(state);
2972
+ return;
2973
+ }
2974
+ }
1461
2975
  const loadAll = client._tokenStore.loadAllSeqs?.bind(client._tokenStore);
1462
2976
  if (typeof loadAll === 'function') {
1463
2977
  let state = await loadAll(aid, deviceId, slotId);
@@ -1494,71 +3008,7 @@ export class MessageDeliveryEngine {
1494
3008
  }
1495
3009
  }
1496
3010
  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;
3011
+ return state;
1562
3012
  }
1563
3013
  async saveSeqTrackerState() {
1564
3014
  const client = this.runtime.client;
@@ -1567,7 +3017,9 @@ export class MessageDeliveryEngine {
1567
3017
  await this.flushSeqTrackerPending();
1568
3018
  return;
1569
3019
  }
1570
- const state = client._seqTracker.exportState();
3020
+ const state = typeof client._seqTracker.exportSyncState === 'function'
3021
+ ? client._seqTracker.exportSyncState()
3022
+ : Object.fromEntries(Object.entries(client._seqTracker.exportState()).map(([ns, seq]) => [ns, { ack: Number(seq), tail: Number(seq), head: Number(seq) }]));
1571
3023
  const pending = this.seqTrackerPending();
1572
3024
  if (pending.size > 0 && client._seqTrackerPendingPersistContext === context) {
1573
3025
  const liveNamespaces = new Set(Object.keys(state));
@@ -1595,9 +3047,34 @@ export class MessageDeliveryEngine {
1595
3047
  if (forceSeq !== undefined) {
1596
3048
  client._seqTracker.forceContiguousSeq(ns, seq);
1597
3049
  }
1598
- this.mergeSeqTrackerPending(context, { [ns]: seq });
3050
+ const state = typeof client._seqTracker.getSyncState === 'function'
3051
+ ? client._seqTracker.getSyncState(ns)
3052
+ : { ack: seq, tail: seq, head: seq };
3053
+ this.mergeSeqTrackerPending(context, { [ns]: state });
1599
3054
  this.scheduleSeqTrackerFlush();
1600
3055
  }
3056
+ async commitSeqTrackerState(ns) {
3057
+ const client = this.runtime.client;
3058
+ const context = this.currentSeqTrackerContext();
3059
+ if (!context || !ns)
3060
+ return;
3061
+ const current = client._seqTracker.getContiguousSeq(ns);
3062
+ const state = typeof client._seqTracker.getSyncState === 'function'
3063
+ ? client._seqTracker.getSyncState(ns)
3064
+ : { ack: current, tail: current, head: current };
3065
+ if (![state.ack, state.tail, state.head].some((value) => Number(value) > 0))
3066
+ return;
3067
+ const [aid, deviceId, slotId] = JSON.parse(context);
3068
+ const pendingBeforeCommit = this.takeSeqTrackerPending(context, ns);
3069
+ try {
3070
+ await this.writeSeqTrackerState(aid, deviceId, slotId, { [ns]: state }, true);
3071
+ }
3072
+ catch (exc) {
3073
+ this.restoreSeqTrackerPending(context, ns, pendingBeforeCommit);
3074
+ throw exc;
3075
+ }
3076
+ client._clientLog.debug(`seq_tracker atomic commit: ns=${ns} A/T/H=${JSON.stringify(state)} context=${context}`);
3077
+ }
1601
3078
  async flushSeqTrackerPending() {
1602
3079
  const client = this.runtime.client;
1603
3080
  this.cancelSeqTrackerFlushTimer();
@@ -1622,6 +3099,34 @@ export class MessageDeliveryEngine {
1622
3099
  this.cancelSeqTrackerFlushTimer();
1623
3100
  }
1624
3101
  }
3102
+ takeSeqTrackerPending(context, ns) {
3103
+ const client = this.runtime.client;
3104
+ if (client._seqTrackerPendingPersistContext !== context)
3105
+ return null;
3106
+ const pending = this.seqTrackerPending();
3107
+ const state = pending.get(ns);
3108
+ if (!state)
3109
+ return null;
3110
+ pending.delete(ns);
3111
+ if (pending.size === 0) {
3112
+ client._seqTrackerPendingPersistContext = null;
3113
+ this.cancelSeqTrackerFlushTimer();
3114
+ }
3115
+ return { ...state };
3116
+ }
3117
+ restoreSeqTrackerPending(context, ns, state) {
3118
+ if (!state || this.currentSeqTrackerContext() !== context)
3119
+ return;
3120
+ const client = this.runtime.client;
3121
+ const pendingContext = client._seqTrackerPendingPersistContext;
3122
+ if (pendingContext && pendingContext !== context)
3123
+ return;
3124
+ const pending = this.seqTrackerPending();
3125
+ if (!pending.has(ns))
3126
+ pending.set(ns, state);
3127
+ client._seqTrackerPendingPersistContext = context;
3128
+ this.scheduleSeqTrackerFlush();
3129
+ }
1625
3130
  currentSeqTrackerContext() {
1626
3131
  const client = this.runtime.client;
1627
3132
  if (!client._aid)
@@ -1643,10 +3148,12 @@ export class MessageDeliveryEngine {
1643
3148
  void this.flushSeqTrackerPending();
1644
3149
  }
1645
3150
  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);
3151
+ for (const [ns, raw] of Object.entries(state)) {
3152
+ const ack = Number(raw.ack);
3153
+ const tail = Number(raw.tail);
3154
+ const head = Number(raw.head);
3155
+ if (ns && [ack, tail, head].every(Number.isFinite) && Math.max(ack, tail, head) > 0) {
3156
+ pending.set(ns, { ack, tail, head });
1650
3157
  }
1651
3158
  }
1652
3159
  }
@@ -1669,14 +3176,27 @@ export class MessageDeliveryEngine {
1669
3176
  client._seqTrackerFlushTimer = null;
1670
3177
  }
1671
3178
  }
1672
- async writeSeqTrackerState(aid, deviceId, slotId, state) {
3179
+ async writeSeqTrackerState(aid, deviceId, slotId, state, throwOnError = false) {
3180
+ await this.enqueueSeqTrackerWrite(() => this.writeSeqTrackerStateNow(aid, deviceId, slotId, state, throwOnError));
3181
+ }
3182
+ enqueueSeqTrackerWrite(operation) {
3183
+ const run = this.seqTrackerWriteChain.then(operation, operation);
3184
+ this.seqTrackerWriteChain = run.catch(() => { });
3185
+ return run;
3186
+ }
3187
+ async writeSeqTrackerStateNow(aid, deviceId, slotId, state, throwOnError = false) {
1673
3188
  const client = this.runtime.client;
1674
3189
  if (!aid || Object.keys(state).length === 0)
1675
3190
  return;
1676
3191
  try {
3192
+ const saveSync = client._tokenStore.saveSyncStates?.bind(client._tokenStore);
3193
+ if (typeof saveSync === 'function') {
3194
+ await saveSync(aid, deviceId, slotId, state);
3195
+ return;
3196
+ }
1677
3197
  const saveFn = client._tokenStore.saveSeq?.bind(client._tokenStore);
1678
3198
  if (typeof saveFn === 'function') {
1679
- await Promise.all(Object.entries(state).map(([ns, seq]) => saveFn(aid, deviceId, slotId, ns, seq)));
3199
+ await Promise.all(Object.entries(state).map(([ns, triplet]) => saveFn(aid, deviceId, slotId, ns, triplet.ack)));
1680
3200
  return;
1681
3201
  }
1682
3202
  if (typeof client._tokenStore.updateInstanceState === 'function') {
@@ -1690,69 +3210,99 @@ export class MessageDeliveryEngine {
1690
3210
  }
1691
3211
  }
1692
3212
  catch (exc) {
3213
+ const error = formatDeliveryError(exc);
3214
+ client._clientLog.warn(`save SeqTracker state failed: ${error}`);
1693
3215
  client._dispatcher.publish('seq_tracker.persist_error', {
1694
3216
  phase: 'save',
1695
3217
  aid,
1696
3218
  device_id: deviceId,
1697
3219
  slot_id: slotId,
1698
- error: String(exc),
3220
+ error: String(error),
1699
3221
  }).catch(() => { });
3222
+ if (throwOnError)
3223
+ throw exc;
1700
3224
  }
1701
3225
  }
1702
3226
  persistRepairedSeq(ns) {
1703
3227
  const client = this.runtime.client;
1704
3228
  if (!client._aid || !ns)
1705
3229
  return;
3230
+ const aid = client._aid;
3231
+ const deviceId = client._deviceId;
3232
+ const slotId = client._slotId;
1706
3233
  const seq = client._seqTracker.getContiguousSeq(ns);
1707
3234
  this.dropSeqTrackerPending(ns);
1708
- try {
1709
- if (seq > 0 && typeof client._tokenStore.saveSeq === 'function') {
1710
- client._tokenStore.saveSeq(client._aid, client._deviceId, client._slotId, ns, seq).catch((exc) => {
3235
+ const saveSeq = client._tokenStore.saveSeq;
3236
+ if (seq > 0 && typeof saveSeq === 'function') {
3237
+ void this.enqueueSeqTrackerWrite(async () => {
3238
+ try {
3239
+ await saveSeq.call(client._tokenStore, aid, deviceId, slotId, ns, seq);
3240
+ }
3241
+ catch (exc) {
1711
3242
  client._clientLog.debug(`persist repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
1712
- });
1713
- return;
1714
- }
1715
- const deleteSeq = client._tokenStore.deleteSeq;
1716
- if (seq <= 0 && typeof deleteSeq === 'function') {
1717
- deleteSeq.call(client._tokenStore, client._aid, client._deviceId, client._slotId, ns).catch((exc) => {
3243
+ }
3244
+ });
3245
+ return;
3246
+ }
3247
+ const deleteSeq = client._tokenStore.deleteSeq;
3248
+ if (seq <= 0 && typeof deleteSeq === 'function') {
3249
+ void this.enqueueSeqTrackerWrite(async () => {
3250
+ try {
3251
+ await deleteSeq.call(client._tokenStore, aid, deviceId, slotId, ns);
3252
+ }
3253
+ catch (exc) {
1718
3254
  client._clientLog.debug(`delete repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
1719
- });
1720
- return;
1721
- }
1722
- if (seq <= 0 && typeof client._tokenStore.updateInstanceState === 'function') {
1723
- Promise.resolve(client._tokenStore.updateInstanceState(client._aid, client._deviceId, client._slotId, (current) => {
1724
- if (isJsonObject(current.seq_tracker_state)) {
1725
- const next = { ...current.seq_tracker_state };
1726
- delete next[ns];
1727
- current.seq_tracker_state = next;
1728
- }
1729
- return current;
1730
- })).catch((exc) => {
3255
+ }
3256
+ });
3257
+ return;
3258
+ }
3259
+ const updater = client._tokenStore.updateInstanceState;
3260
+ if (seq <= 0 && typeof updater === 'function') {
3261
+ void this.enqueueSeqTrackerWrite(async () => {
3262
+ try {
3263
+ await updater.call(client._tokenStore, aid, deviceId, slotId, (current) => {
3264
+ if (isJsonObject(current.seq_tracker_state)) {
3265
+ const next = { ...current.seq_tracker_state };
3266
+ delete next[ns];
3267
+ current.seq_tracker_state = next;
3268
+ }
3269
+ return current;
3270
+ });
3271
+ }
3272
+ catch (exc) {
1731
3273
  client._clientLog.debug(`delete repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
1732
- });
1733
- return;
1734
- }
1735
- if (seq > 0) {
1736
- void this.saveSeqTrackerState();
1737
- }
3274
+ }
3275
+ });
3276
+ return;
1738
3277
  }
1739
- catch (exc) {
1740
- client._clientLog.debug(`persist repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
3278
+ if (seq > 0) {
3279
+ void this.saveSeqTrackerState();
1741
3280
  }
1742
3281
  }
1743
3282
  clampAckSeq(method, field, ns, seq) {
1744
3283
  const client = this.runtime.client;
1745
3284
  const original = seq;
1746
- let next = Number(seq);
1747
- if (!Number.isFinite(next))
3285
+ let next = seq;
3286
+ if (typeof next !== 'number' || !Number.isSafeInteger(next))
1748
3287
  return 0;
1749
3288
  if (next < 0)
1750
3289
  next = 0;
1751
3290
  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;
3291
+ const committedAck = client._seqTracker.getContiguousSeq(ns);
3292
+ if (next > committedAck) {
3293
+ client._clientLog.warn(`ack clamp: method=${method} ${field}=${original} > committed_ack=${committedAck}, clamp`);
3294
+ next = committedAck;
3295
+ }
3296
+ if ((method === 'message.v2.ack' || method === 'message.ack')) {
3297
+ const pending = this.pendingP2PInlineAcks?.get(ns);
3298
+ if (pending && !pending.committed && next >= pending.seq)
3299
+ next = pending.seq - 1;
3300
+ }
3301
+ else if (method === 'group.v2.ack' || method === 'group.ack_messages') {
3302
+ const canonicalNs = this.resolvePendingGroupInlineAckNamespace(ns);
3303
+ const pending = this.pendingGroupInlineAcks?.get(canonicalNs);
3304
+ if (pending && !pending.committed && next >= pending.seq)
3305
+ next = pending.seq - 1;
1756
3306
  }
1757
3307
  }
1758
3308
  return next;
@@ -1761,7 +3311,8 @@ export class MessageDeliveryEngine {
1761
3311
  const client = this.runtime.client;
1762
3312
  const clampField = (field, ns) => {
1763
3313
  const raw = params[field];
1764
- if (typeof raw !== 'number' || !Number.isFinite(raw))
3314
+ // 保留非法类型/数值,让 V2 ACK 严格入口失败关闭,不能先 clamp 成合法游标。
3315
+ if (typeof raw !== 'number' || !Number.isSafeInteger(raw))
1765
3316
  return params;
1766
3317
  const next = this.clampAckSeq(method, field, ns, raw);
1767
3318
  return next === raw ? params : { ...params, [field]: next };
@@ -1775,7 +3326,7 @@ export class MessageDeliveryEngine {
1775
3326
  return clampField('seq', ns);
1776
3327
  }
1777
3328
  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();
3329
+ const groupId = String(params.group_id ?? params.group_aid ?? '').trim();
1779
3330
  const ns = groupId ? `group:${groupId}` : '';
1780
3331
  if (method === 'group.v2.ack')
1781
3332
  return clampField('up_to_seq', ns);
@@ -1786,7 +3337,7 @@ export class MessageDeliveryEngine {
1786
3337
  }
1787
3338
  return params;
1788
3339
  }
1789
- async drainOrderedMessages(ns, beforeSeq) {
3340
+ async drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true) {
1790
3341
  const client = this.runtime.client;
1791
3342
  const queue = client._pendingOrderedMsgs.get(ns);
1792
3343
  if (!queue || queue.size === 0)
@@ -1801,13 +3352,13 @@ export class MessageDeliveryEngine {
1801
3352
  queue.delete(seq);
1802
3353
  if (!item || client._pushedSeqs.get(ns)?.has(seq))
1803
3354
  continue;
1804
- await this.publishOrderedQueueItem(ns, item.event, seq, item.payload);
3355
+ await this.publishOrderedQueueItem(ns, item.event, seq, item.payload, pullResponse);
1805
3356
  this.markPublishedSeq(ns, seq);
1806
3357
  delivered = true;
1807
3358
  }
1808
3359
  if (queue.size === 0) {
1809
3360
  client._pendingOrderedMsgs.delete(ns);
1810
- if (delivered)
3361
+ if (delivered && persist)
1811
3362
  await this.saveSeqTrackerState();
1812
3363
  }
1813
3364
  }
@@ -1830,7 +3381,7 @@ export class MessageDeliveryEngine {
1830
3381
  this.enqueueOrderedMessage(ns, event, seqNum, payload);
1831
3382
  return false;
1832
3383
  }
1833
- await this.drainOrderedMessages(ns, seqNum);
3384
+ await this.drainOrderedMessages(ns, seqNum, true);
1834
3385
  if (client._pushedSeqs.get(ns)?.has(seqNum))
1835
3386
  return false;
1836
3387
  const queue = client._pendingOrderedMsgs.get(ns);
@@ -1844,14 +3395,14 @@ export class MessageDeliveryEngine {
1844
3395
  await this.saveSeqTrackerState();
1845
3396
  return true;
1846
3397
  }
1847
- async publishPulledMessage(event, ns, seq, payload) {
3398
+ async publishPulledMessage(event, ns, seq, payload, persist = true) {
1848
3399
  const client = this.runtime.client;
1849
3400
  const seqNum = Number(seq);
1850
3401
  if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0 || !ns) {
1851
3402
  if (event === 'message.recalled') {
1852
- return this.publishMessageRecallTombstone(seq, payload);
3403
+ return await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seq, payload));
1853
3404
  }
1854
- await client._publishAppEvent(event, payload);
3405
+ await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload));
1855
3406
  return true;
1856
3407
  }
1857
3408
  const queue = client._pendingOrderedMsgs.get(ns);
@@ -1861,16 +3412,16 @@ export class MessageDeliveryEngine {
1861
3412
  client._pendingOrderedMsgs.delete(ns);
1862
3413
  return false;
1863
3414
  }
1864
- await this.drainOrderedMessages(ns, seqNum);
3415
+ await this.drainOrderedMessages(ns, seqNum, false, persist);
1865
3416
  queue?.delete(seqNum);
1866
3417
  if (queue && queue.size === 0)
1867
3418
  client._pendingOrderedMsgs.delete(ns);
1868
3419
  if (event === 'message.recalled') {
1869
- const published = await this.publishMessageRecallTombstone(seqNum, payload);
3420
+ const published = await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seqNum, payload));
1870
3421
  this.markPublishedSeq(ns, seqNum);
1871
3422
  return published;
1872
3423
  }
1873
- await client._publishAppEvent(event, payload);
3424
+ await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload));
1874
3425
  this.markPublishedSeq(ns, seqNum);
1875
3426
  return true;
1876
3427
  }