@agentunion/fastaun 0.5.6 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/_packed_docs/CHANGELOG.md +106 -0
  3. package/_packed_docs/INDEX.md +12 -11
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +4 -3
  5. package/_packed_docs/agent.md/SCHEMA.md +83 -58
  6. package/_packed_docs/agent.md/examples/codeagent-claudecode.md +1 -1
  7. package/_packed_docs/agent.md/examples/openclaw-lobster.md +1 -1
  8. package/_packed_docs/agent.md/examples/signed-openclaw-lobster.md +1 -1
  9. package/_packed_docs/agent.md/examples//347/276/244/347/273/204-/345/274/200/345/217/221/345/233/242/351/230/237.md +21 -0
  10. package/_packed_docs/audit/AUN/346/234/215/345/212/241Go/345/214/226/351/207/215/346/236/204/347/262/276/347/273/206/345/214/226/346/272/220/347/240/201/345/256/241/346/237/245-20260718.md +3 -3
  11. package/_packed_docs/aun/345/205/254/347/275/221/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +7 -0
  12. package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +422 -331
  13. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +598 -398
  14. package/_packed_docs/group-message-rpc-alignment-gaps.md +741 -0
  15. package/_packed_docs/message-online-push-alignment.md +572 -0
  16. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1 -1
  17. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +10 -10
  18. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +2 -2
  19. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +30 -10
  20. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +23 -20
  21. package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +3 -3
  22. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +5 -6
  23. package/_packed_docs/sdk/09-group-rpc-manual.md +11 -7
  24. package/_packed_docs/sdk/09-message-rpc-manual.md +5 -3
  25. package/_packed_docs/sdk/09-storage-rpc-manual.md +14 -3
  26. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +19 -17
  27. package/_packed_docs/sdk/CHANGELOG-0.5.6.md +73 -0
  28. package/_packed_docs/sdk/INDEX.md +22 -22
  29. package/_packed_docs/sdk/README.md +6 -5
  30. package/dist/agent-md-schema.d.ts +16 -0
  31. package/dist/agent-md-schema.js +109 -0
  32. package/dist/agent-md-schema.js.map +1 -0
  33. package/dist/agent-md.d.ts +7 -0
  34. package/dist/agent-md.js +91 -9
  35. package/dist/agent-md.js.map +1 -1
  36. package/dist/aid-store.d.ts +5 -1
  37. package/dist/aid-store.js +25 -6
  38. package/dist/aid-store.js.map +1 -1
  39. package/dist/aid.d.ts +2 -0
  40. package/dist/aid.js +32 -12
  41. package/dist/aid.js.map +1 -1
  42. package/dist/auth.d.ts +3 -1
  43. package/dist/auth.js +65 -13
  44. package/dist/auth.js.map +1 -1
  45. package/dist/cert-utils.d.ts +2 -1
  46. package/dist/cert-utils.js +32 -16
  47. package/dist/cert-utils.js.map +1 -1
  48. package/dist/client/delivery.d.ts +36 -7
  49. package/dist/client/delivery.js +701 -216
  50. package/dist/client/delivery.js.map +1 -1
  51. package/dist/client/group-state.js +2 -2
  52. package/dist/client/group-state.js.map +1 -1
  53. package/dist/client/lifecycle.d.ts +15 -1
  54. package/dist/client/lifecycle.js +475 -152
  55. package/dist/client/lifecycle.js.map +1 -1
  56. package/dist/client/rpc-pipeline.d.ts +24 -6
  57. package/dist/client/rpc-pipeline.js +372 -131
  58. package/dist/client/rpc-pipeline.js.map +1 -1
  59. package/dist/client/runtime.d.ts +1 -3
  60. package/dist/client/runtime.js +4 -7
  61. package/dist/client/runtime.js.map +1 -1
  62. package/dist/client/v2-e2ee.d.ts +20 -3
  63. package/dist/client/v2-e2ee.js +376 -93
  64. package/dist/client/v2-e2ee.js.map +1 -1
  65. package/dist/client.d.ts +23 -10
  66. package/dist/client.js +515 -150
  67. package/dist/client.js.map +1 -1
  68. package/dist/discovery.d.ts +1 -0
  69. package/dist/discovery.js +3 -0
  70. package/dist/discovery.js.map +1 -1
  71. package/dist/facades.d.ts +23 -1
  72. package/dist/facades.js +59 -7
  73. package/dist/facades.js.map +1 -1
  74. package/dist/group-id.js +2 -17
  75. package/dist/group-id.js.map +1 -1
  76. package/dist/index.d.ts +4 -3
  77. package/dist/index.js +2 -1
  78. package/dist/index.js.map +1 -1
  79. package/dist/net.d.ts +3 -0
  80. package/dist/net.js +13 -0
  81. package/dist/net.js.map +1 -1
  82. package/dist/seq-tracker.js +6 -1
  83. package/dist/seq-tracker.js.map +1 -1
  84. package/dist/tools/cross-sdk-agent.js +29 -3
  85. package/dist/tools/cross-sdk-agent.js.map +1 -1
  86. package/dist/transport.js +8 -1
  87. package/dist/transport.js.map +1 -1
  88. package/dist/validators.js +8 -14
  89. package/dist/validators.js.map +1 -1
  90. package/dist/version.d.ts +1 -1
  91. package/dist/version.js +1 -1
  92. package/package.json +4 -3
@@ -37,23 +37,235 @@ function isPromiseLike(value) {
37
37
  function formatDeliveryError(error) {
38
38
  return error instanceof Error ? error : String(error);
39
39
  }
40
+ function positiveSafeSequenceHint(value) {
41
+ if (typeof value === 'number')
42
+ return Number.isSafeInteger(value) && value > 0 ? value : 0;
43
+ if (typeof value !== 'string' || !value || value.trim() !== value)
44
+ return 0;
45
+ const parsed = Number(value);
46
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 0;
47
+ }
48
+ function nonNegativeSafeSequenceHint(value) {
49
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
50
+ }
51
+ function deduplicatePlainForwardPageMessages(value, afterSeq) {
52
+ if (!Array.isArray(value))
53
+ return [];
54
+ const bySeq = new Map();
55
+ for (const raw of value) {
56
+ if (!isJsonObject(raw))
57
+ continue;
58
+ const seq = positiveSafeSequenceHint(raw.seq);
59
+ if (seq <= afterSeq || bySeq.has(seq))
60
+ continue;
61
+ bySeq.set(seq, { ...raw, seq });
62
+ }
63
+ return [...bySeq.entries()].sort(([left], [right]) => left - right).map(([, message]) => message);
64
+ }
40
65
  export class MessageDeliveryEngine {
41
66
  runtime;
42
67
  realtimeTailHeads = null;
43
68
  realtimeSyncing = null;
69
+ pendingGroupPullUpper = null;
70
+ onlineUnreadHintTargets = null;
71
+ onlineUnreadHintTasks = null;
44
72
  pendingP2PInlineAcks = null;
45
73
  pendingGroupInlineAcks = null;
46
74
  pendingGroupInlineAckNamespaces = null;
47
75
  inlineAckGeneration = 0;
76
+ // 同一实例的 tracker 写入必须串行,避免旧的延迟 flush 覆盖新的 Forward 提交。
77
+ seqTrackerWriteChain = Promise.resolve();
48
78
  constructor(runtime) {
49
79
  this.runtime = runtime;
50
80
  }
81
+ isPullOperationCurrent() {
82
+ const client = this.runtime.client;
83
+ const generation = client._pullOperationGeneration;
84
+ if (generation === undefined)
85
+ return true;
86
+ const pipeline = client._rpcPipeline;
87
+ return typeof pipeline?.isPullGenerationCurrent === 'function'
88
+ ? pipeline.isPullGenerationCurrent(generation)
89
+ : true;
90
+ }
91
+ ensurePullOperationCurrent() {
92
+ if (!this.isPullOperationCurrent())
93
+ throw new Error('pull invalidated');
94
+ }
95
+ forwardCoordinator() {
96
+ const coordinator = this.runtime.client._v2E2EE;
97
+ if (!coordinator
98
+ || typeof coordinator.pendingForwardCursor !== 'function'
99
+ || typeof coordinator.recordForwardCursor !== 'function'
100
+ || typeof coordinator.clearForwardCursor !== 'function'
101
+ || typeof coordinator.pendingForwardAck !== 'function'
102
+ || typeof coordinator.recordForwardAck !== 'function'
103
+ || typeof coordinator.clearForwardAck !== 'function') {
104
+ throw new Error('Forward cursor coordinator is unavailable');
105
+ }
106
+ return coordinator;
107
+ }
108
+ async confirmPlainForwardAck(ns, method, ackSeq, groupId = '') {
109
+ const client = this.runtime.client;
110
+ this.ensurePullOperationCurrent();
111
+ const coordinator = this.forwardCoordinator();
112
+ const generation = this.captureInlineAckGeneration();
113
+ coordinator.recordForwardAck(ns, ackSeq);
114
+ const params = method === 'message.ack'
115
+ ? {
116
+ seq: ackSeq,
117
+ device_id: client._deviceId,
118
+ slot_id: client._slotId,
119
+ _rpc_background: true,
120
+ }
121
+ : {
122
+ group_id: groupId,
123
+ msg_seq: ackSeq,
124
+ device_id: client._deviceId,
125
+ slot_id: client._slotId,
126
+ _rpc_background: true,
127
+ };
128
+ const result = await client._rpcPipeline.rawCall(method, params, { background: true });
129
+ this.ensurePullOperationCurrent();
130
+ const actualAckSeq = this.resolveForwardAckSeq(result, ackSeq);
131
+ if (actualAckSeq < ackSeq) {
132
+ throw new Error(`${method} server ACK watermark ${actualAckSeq} is below requested ${ackSeq}`);
133
+ }
134
+ coordinator.clearForwardAck(ns, actualAckSeq);
135
+ if (method === 'message.ack') {
136
+ this.onP2PV2AckSucceeded(actualAckSeq, generation);
137
+ }
138
+ else {
139
+ this.onGroupV2AckSucceeded(groupId, actualAckSeq, generation);
140
+ }
141
+ }
142
+ async commitPlainForwardPage(ns, afterSeq, method, result, groupId = '') {
143
+ if (!isJsonObject(result)) {
144
+ throw new Error(`${method} response must be an object`);
145
+ }
146
+ const client = this.runtime.client;
147
+ this.ensurePullOperationCurrent();
148
+ const response = result;
149
+ const messages = deduplicatePlainForwardPageMessages(response.messages, afterSeq);
150
+ const coordinator = this.forwardCoordinator();
151
+ const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
152
+ const pageInlineGeneration = this.captureInlineAckGeneration();
153
+ const deferredServerCursor = Number(coordinator.pendingForwardCursor(ns, pageContigBefore) ?? 0);
154
+ const deferredForwardAck = Number(coordinator.pendingForwardAck(ns, pageContigBefore) ?? 0);
155
+ const pageTrackerSnapshot = typeof client._seqTracker.snapshotNamespace === 'function'
156
+ ? client._seqTracker.snapshotNamespace(ns)
157
+ : null;
158
+ response._contig_before = pageContigBefore;
159
+ const cursor = isJsonObject(response.cursor)
160
+ ? response.cursor
161
+ : null;
162
+ const serverAckRaw = method === 'message.pull' ? response.server_ack_seq : cursor?.current_seq;
163
+ const hasServerAck = method === 'message.pull'
164
+ ? Object.prototype.hasOwnProperty.call(response, 'server_ack_seq')
165
+ : Boolean(cursor && Object.prototype.hasOwnProperty.call(cursor, 'current_seq'));
166
+ const parsedServerAck = hasServerAck ? nonNegativeSafeSequenceHint(serverAckRaw) : null;
167
+ const serverAckSeq = parsedServerAck ?? 0;
168
+ const retentionFloor = method === 'message.pull'
169
+ ? positiveSafeSequenceHint(response.retention_floor_seq)
170
+ : Math.max(positiveSafeSequenceHint(response.retention_floor_message_seq), positiveSafeSequenceHint(response.retention_floor_seq), positiveSafeSequenceHint(cursor?.retention_floor_seq));
171
+ const visibilityFloor = method === 'group.pull'
172
+ ? Math.max(positiveSafeSequenceHint(cursor?.join_seq), Math.max(0, positiveSafeSequenceHint(response.earliest_available_message_seq) - 1))
173
+ : 0;
174
+ let publishedCount = 0;
175
+ let committed = false;
176
+ try {
177
+ for (const rawMessage of messages) {
178
+ this.ensurePullOperationCurrent();
179
+ const seq = positiveSafeSequenceHint(rawMessage.seq);
180
+ if (method === 'message.pull') {
181
+ const appEvent = this.p2pAppEventForMessage(rawMessage);
182
+ if (await this.publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false)) {
183
+ publishedCount += 1;
184
+ }
185
+ this.ensurePullOperationCurrent();
186
+ continue;
187
+ }
188
+ const message = normalizeGroupMentionMode(rawMessage);
189
+ if (this.recallEventFromGroupMessage(message)) {
190
+ if (await this.publishGroupRecallTombstone(groupId, seq, message)) {
191
+ this.ensurePullOperationCurrent();
192
+ this.markPublishedSeq(ns, seq);
193
+ publishedCount += 1;
194
+ }
195
+ }
196
+ else if (await this.publishPulledMessage('group.message_created', ns, seq, message, false)) {
197
+ this.ensurePullOperationCurrent();
198
+ publishedCount += 1;
199
+ }
200
+ }
201
+ this.ensurePullOperationCurrent();
202
+ if (messages.length > 0)
203
+ client._seqTracker.onPullResult(ns, messages, afterSeq);
204
+ const commitTarget = Math.max(client._seqTracker.getContiguousSeq(ns), retentionFloor, visibilityFloor, deferredServerCursor);
205
+ if (commitTarget > client._seqTracker.getContiguousSeq(ns)) {
206
+ client._seqTracker.forceContiguousSeq(ns, commitTarget);
207
+ }
208
+ if (client._seqTracker.getContiguousSeq(ns) !== pageContigBefore) {
209
+ this.ensurePullOperationCurrent();
210
+ await this.drainOrderedMessages(ns, undefined, false, false);
211
+ this.ensurePullOperationCurrent();
212
+ await client._commitSeqTrackerState(ns);
213
+ }
214
+ committed = true;
215
+ }
216
+ catch (exc) {
217
+ if (this.isPullOperationCurrent() && !committed && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
218
+ client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
219
+ this.dropSeqTrackerPending(ns);
220
+ }
221
+ throw exc;
222
+ }
223
+ this.ensurePullOperationCurrent();
224
+ const committedAck = client._seqTracker.getContiguousSeq(ns);
225
+ if (deferredServerCursor > 0 && committedAck >= deferredServerCursor) {
226
+ coordinator.clearForwardCursor(ns, committedAck);
227
+ }
228
+ if (hasServerAck && parsedServerAck !== null && serverAckSeq > committedAck) {
229
+ coordinator.recordForwardCursor(ns, serverAckSeq, committedAck);
230
+ }
231
+ if (hasServerAck && parsedServerAck !== null && serverAckSeq >= deferredForwardAck) {
232
+ coordinator.clearForwardAck(ns, serverAckSeq);
233
+ if (method === 'message.pull') {
234
+ this.onP2PV2AckSucceeded(serverAckSeq, pageInlineGeneration);
235
+ }
236
+ else {
237
+ this.onGroupV2AckSucceeded(groupId, serverAckSeq, pageInlineGeneration);
238
+ }
239
+ }
240
+ const contigAdvanced = committedAck > pageContigBefore;
241
+ const knownServerAck = hasServerAck ? serverAckSeq : deferredServerCursor;
242
+ if (committedAck > 0
243
+ && ((hasServerAck && committedAck > serverAckSeq)
244
+ || (contigAdvanced && committedAck > knownServerAck))) {
245
+ coordinator.recordForwardAck(ns, committedAck);
246
+ }
247
+ const pendingAckSeq = Number(coordinator.pendingForwardAck(ns, committedAck) ?? 0);
248
+ if (pendingAckSeq > 0) {
249
+ const ackMethod = method === 'message.pull' ? 'message.ack' : 'group.ack_messages';
250
+ const clampedAckSeq = this.clampAckSeq(ackMethod, ackMethod === 'message.ack' ? 'seq' : 'msg_seq', ns, pendingAckSeq);
251
+ if (clampedAckSeq < pendingAckSeq) {
252
+ throw new Error(`${ackMethod} cannot confirm pending Forward watermark ${pendingAckSeq}`);
253
+ }
254
+ this.ensurePullOperationCurrent();
255
+ await this.confirmPlainForwardAck(ns, ackMethod, pendingAckSeq, groupId);
256
+ }
257
+ return { rawCount: messages.length, publishedCount };
258
+ }
51
259
  resetInlineAckState() {
52
260
  this.inlineAckGeneration += 1;
261
+ void this.runtime.client._rpcPipeline?.invalidatePulls?.();
53
262
  this.pendingP2PInlineAcks = null;
54
263
  this.pendingGroupInlineAcks = null;
55
264
  this.pendingGroupInlineAckNamespaces = null;
56
265
  this.realtimeSyncing = null;
266
+ this.pendingGroupPullUpper = null;
267
+ this.onlineUnreadHintTargets = null;
268
+ this.onlineUnreadHintTasks = null;
57
269
  this.realtimeTailHeads = null;
58
270
  }
59
271
  inlineGenerationIsCurrent(generation) {
@@ -68,6 +280,15 @@ export class MessageDeliveryEngine {
68
280
  captureGroupInlineAckGeneration() {
69
281
  return this.pendingGroupInlineAcks?.size ? this.inlineAckGeneration : null;
70
282
  }
283
+ async waitForPendingInlineCommit(ns, seq) {
284
+ const pending = this.pendingP2PInlineAcks?.get(ns);
285
+ if (pending && (pending.committed || pending.seq > seq))
286
+ return;
287
+ const queues = this.runtime.client._pushProcessQueues;
288
+ const processing = queues?.get(ns);
289
+ if (processing)
290
+ await processing;
291
+ }
71
292
  pendingInlineAckNamespaceCount() {
72
293
  return (this.pendingP2PInlineAcks?.size ?? 0) + (this.pendingGroupInlineAcks?.size ?? 0);
73
294
  }
@@ -90,6 +311,50 @@ export class MessageDeliveryEngine {
90
311
  endRealtimeSync(ns) {
91
312
  this.realtimeSyncing?.delete(ns);
92
313
  }
314
+ hasPendingPull(ns) {
315
+ return (this.pendingGroupPullUpper?.get(ns) ?? 0) > 0;
316
+ }
317
+ onPullGateIdle(ns) {
318
+ if (!ns)
319
+ return;
320
+ if (ns.startsWith('p2p:')) {
321
+ this.runtime.client._schedulePendingP2pPullIfNeeded?.(ns, 'pull-gate-idle');
322
+ return;
323
+ }
324
+ this.schedulePendingGroupPullIfNeeded(ns, 'pull-gate-idle');
325
+ }
326
+ recordPendingGroupPull(ns, seq) {
327
+ if (!ns || !Number.isSafeInteger(seq) || seq <= 0)
328
+ return;
329
+ const pending = this.pendingGroupPullUpper ?? new Map();
330
+ this.pendingGroupPullUpper = pending;
331
+ pending.set(ns, Math.max(pending.get(ns) ?? 0, seq));
332
+ }
333
+ schedulePendingGroupPullIfNeeded(ns, reason) {
334
+ const pending = this.pendingGroupPullUpper;
335
+ if (!pending)
336
+ return false;
337
+ const upper = pending.get(ns) ?? 0;
338
+ if (upper <= 0) {
339
+ pending.delete(ns);
340
+ return false;
341
+ }
342
+ const client = this.runtime.client;
343
+ const contiguous = client._seqTracker.getContiguousSeq(ns);
344
+ if (upper <= contiguous) {
345
+ pending.delete(ns);
346
+ client._clientLog.debug(`Group pending pull upper already covered: ns=${ns}, upper_seq=${upper}, contiguous=${contiguous}, reason=${reason}`);
347
+ return false;
348
+ }
349
+ if (client.state !== ConnectionState.READY || client._closing
350
+ || client._rpcPipeline?.hasPullActivity?.(ns, false))
351
+ return false;
352
+ pending.delete(ns);
353
+ const groupId = ns.slice('group:'.length);
354
+ client._clientLog.debug(`Group pending push follow-up pull scheduled: ns=${ns}, upper_seq=${upper}, reason=${reason}`);
355
+ client._safeAsync(this.onRawGroupV2MessageCreated({ group_id: groupId, seq: upper }));
356
+ return true;
357
+ }
93
358
  prunePushedSeqs(ns) {
94
359
  const client = this.runtime.client;
95
360
  const pushed = client._pushedSeqs.get(ns);
@@ -132,12 +397,15 @@ export class MessageDeliveryEngine {
132
397
  }
133
398
  async publishOrderedQueueItem(ns, event, seq, payload, source, pullResponse = false) {
134
399
  const client = this.runtime.client;
400
+ this.ensurePullOperationCurrent();
135
401
  if (event === 'group.changed' && this.isGroupEventNamespace(ns)) {
136
402
  await this.publishOrderedGroupChanged(payload, source);
403
+ this.ensurePullOperationCurrent();
137
404
  return;
138
405
  }
139
406
  if (event === 'message.recalled') {
140
407
  await this.publishMessageRecallTombstone(seq, payload);
408
+ this.ensurePullOperationCurrent();
141
409
  return;
142
410
  }
143
411
  if (pullResponse) {
@@ -150,6 +418,7 @@ export class MessageDeliveryEngine {
150
418
  if (isPromiseLike(published))
151
419
  await published;
152
420
  }
421
+ this.ensurePullOperationCurrent();
153
422
  }
154
423
  async publishOrderedGroupChanged(payload, source = 'ordered') {
155
424
  const client = this.runtime.client;
@@ -925,7 +1194,7 @@ export class MessageDeliveryEngine {
925
1194
  // 在旧 seq tracking / ordered publish 之前转入前台 Tail 协调器。
926
1195
  const inlinePresent = Object.prototype.hasOwnProperty.call(msg, 'inline_message');
927
1196
  const legacyPushSeq = this.realtimeHeadSeq(msg.seq, inlinePresent);
928
- if (client._v2Session && client._aid && msg.seq !== undefined && msg.seq !== null) {
1197
+ if (client._v2Session && client._aid && (msg.seq !== undefined && msg.seq !== null || inlinePresent)) {
929
1198
  if (legacyPushSeq === null) {
930
1199
  if (inlinePresent)
931
1200
  await this.recoverInvalidP2PRealtimeHead(`p2p:${client._aid}`, msg.seq);
@@ -1025,7 +1294,7 @@ export class MessageDeliveryEngine {
1025
1294
  // 不能让 payload 路径直接推进旧 ordered publish 状态。
1026
1295
  const inlinePresent = Object.prototype.hasOwnProperty.call(msg, 'inline_message');
1027
1296
  const legacyPushSeq = this.realtimeHeadSeq(seq, inlinePresent);
1028
- if (client._v2Session && groupId && seq !== undefined && seq !== null) {
1297
+ if (client._v2Session && groupId && (seq !== undefined && seq !== null || inlinePresent)) {
1029
1298
  if (legacyPushSeq === null) {
1030
1299
  if (inlinePresent)
1031
1300
  await this.recoverInvalidGroupRealtimeHead(groupId, `group:${groupId}`, seq);
@@ -1139,20 +1408,14 @@ export class MessageDeliveryEngine {
1139
1408
  client._clientLog.debug(`P2P message gap fill start: after_seq=${afterSeq}`);
1140
1409
  let filled = 0;
1141
1410
  try {
1142
- const operation = () => client._pullV2(afterSeq, 50, { skipAutoAck: true, gateLocked: true });
1411
+ // Gap Fill 本身就是 Forward Pull,必须沿用协调器的完整事务:
1412
+ // 原始页提交成功后由协调器负责 Piggyback/终端 ACK。
1413
+ const operation = () => client._pullV2(afterSeq, 50, { gateLocked: true });
1143
1414
  const messages = client._rpcPipeline
1144
1415
  ? await client._rpcPipeline.runPullSerialized(client._rpcPipeline.pullGateKeyForCall('message.pull', { after_seq: afterSeq, limit: 50 }), operation, true)
1145
1416
  : await client._withBackgroundRpc(operation);
1146
1417
  filled = messages.length;
1147
1418
  this.prunePushedSeqs(ns);
1148
- if (client._seqTracker.getContiguousSeq(ns) !== afterSeq) {
1149
- await this.drainOrderedMessages(ns, undefined, true);
1150
- this.persistSeq(ns);
1151
- }
1152
- const contig = client._seqTracker.getContiguousSeq(ns);
1153
- if (contig > 0 && contig !== afterSeq) {
1154
- await client._withBackgroundRpc(() => client._ackV2(contig));
1155
- }
1156
1419
  client._clientLog.debug(`P2P message gap fill done: after_seq=${afterSeq}, filled=${filled}`);
1157
1420
  }
1158
1421
  catch (exc) {
@@ -1160,6 +1423,7 @@ export class MessageDeliveryEngine {
1160
1423
  }
1161
1424
  finally {
1162
1425
  client._gapFillDone.delete(dedupKey);
1426
+ client._schedulePendingP2pPullIfNeeded?.(ns, 'gap-fill-complete');
1163
1427
  }
1164
1428
  }
1165
1429
  async fillGroupGap(groupId) {
@@ -1184,10 +1448,6 @@ export class MessageDeliveryEngine {
1184
1448
  : await client._withBackgroundRpc(operation);
1185
1449
  filled = messages.length;
1186
1450
  this.prunePushedSeqs(ns);
1187
- if (client._seqTracker.getContiguousSeq(ns) !== afterSeq) {
1188
- await this.drainOrderedMessages(ns, undefined, true);
1189
- this.persistSeq(ns);
1190
- }
1191
1451
  client._clientLog.debug(`group message gap fill done: group=${groupId}, after_seq=${afterSeq}, filled=${filled}`);
1192
1452
  }
1193
1453
  catch (exc) {
@@ -1198,9 +1458,41 @@ export class MessageDeliveryEngine {
1198
1458
  if (filled > 0 && client._seqTracker.getContiguousSeq(ns) > afterSeq) {
1199
1459
  void this.fillGroupGap(groupId);
1200
1460
  }
1461
+ this.schedulePendingGroupPullIfNeeded(ns, 'gap-fill-complete');
1201
1462
  }
1202
1463
  }
1203
- async fillGroupEventGap(groupId) {
1464
+ scheduleRealtimeWindowForward(ns, groupId = '') {
1465
+ const client = this.runtime.client;
1466
+ const state = this.syncState(ns);
1467
+ if (client._sessionOptions?.background_sync === false
1468
+ || state.ack >= state.tail - 1)
1469
+ return;
1470
+ groupId = String(groupId ?? '').trim();
1471
+ if (!groupId && (!client._aid || ns !== `p2p:${client._aid}`))
1472
+ return;
1473
+ const key = `${groupId ? 'group' : 'p2p'}_realtime_window_forward:${ns}`;
1474
+ if (client._gapFillDone.has(key))
1475
+ return;
1476
+ client._gapFillDone.set(key, Date.now());
1477
+ client._safeAsync(Promise.resolve().then(async () => {
1478
+ try {
1479
+ if (client._sessionOptions?.background_sync === false
1480
+ || client.state !== ConnectionState.READY || client._closing)
1481
+ return;
1482
+ const live = this.syncState(ns);
1483
+ if (live.ack >= live.tail - 1)
1484
+ return;
1485
+ if (groupId)
1486
+ await this.fillGroupGap(groupId);
1487
+ else
1488
+ await this.fillP2pGap();
1489
+ }
1490
+ finally {
1491
+ client._gapFillDone.delete(key);
1492
+ }
1493
+ }));
1494
+ }
1495
+ async fillGroupEventGap(groupId, options = {}) {
1204
1496
  const client = this.runtime.client;
1205
1497
  if (client._sessionOptions?.background_sync === false)
1206
1498
  return;
@@ -1208,32 +1500,49 @@ export class MessageDeliveryEngine {
1208
1500
  if (!groupId)
1209
1501
  return;
1210
1502
  const ns = `group_event:${groupId}`;
1211
- const afterSeq = client._seqTracker.getContiguousSeq(ns);
1212
- const dedupKey = `group_evt:${groupId}:${afterSeq}`;
1503
+ const requestedAfterSeq = Number(options.afterSeq ?? 0);
1504
+ const afterSeq = Number.isSafeInteger(requestedAfterSeq) && requestedAfterSeq > 0
1505
+ ? requestedAfterSeq
1506
+ : client._seqTracker.getContiguousSeq(ns);
1507
+ const singlePage = options.singlePage === true;
1508
+ const dedupKey = `group_event_pull:${ns}`;
1213
1509
  if (client._gapFillDone.has(dedupKey))
1214
1510
  return;
1215
- const token = client._tryAcquirePullGate(ns);
1216
- if (token === null) {
1217
- client._clientLog.debug(`group event gap fill skipped: pull in-flight group=${groupId}`);
1218
- return;
1511
+ let legacyGateToken = null;
1512
+ if (!client._rpcPipeline) {
1513
+ const acquire = client._tryAcquirePullGate;
1514
+ if (typeof acquire === 'function') {
1515
+ legacyGateToken = acquire.call(client, ns);
1516
+ if (legacyGateToken === null)
1517
+ return;
1518
+ }
1219
1519
  }
1220
1520
  client._gapFillDone.set(dedupKey, Date.now());
1221
1521
  let filled = 0;
1522
+ let continuationAfterSeq = 0;
1222
1523
  try {
1223
1524
  let nextAfterSeq = afterSeq;
1224
- const maxPages = 100;
1525
+ const maxPages = singlePage ? 1 : 100;
1225
1526
  let pageCount = 0;
1226
1527
  while (pageCount < maxPages) {
1227
1528
  pageCount += 1;
1228
1529
  client._clientLog.debug(`group event gap fill start: group=${groupId}, after_seq=${nextAfterSeq}`);
1229
- const result = await client.call('group.pull_events', {
1530
+ const request = {
1230
1531
  group_id: groupId,
1231
1532
  after_event_seq: nextAfterSeq,
1232
1533
  device_id: client._deviceId,
1233
1534
  limit: 50,
1234
1535
  _pull_gate_locked: true,
1235
1536
  _rpc_background: true,
1236
- });
1537
+ };
1538
+ let result;
1539
+ if (client._rpcPipeline) {
1540
+ const pullKey = client._rpcPipeline.pullGateKeyForCall('group.pull_events', request);
1541
+ result = await client._rpcPipeline.runPullSerialized(pullKey, () => client.call('group.pull_events', request), true);
1542
+ }
1543
+ else {
1544
+ result = await client.call('group.pull_events', request);
1545
+ }
1237
1546
  if (!isJsonObject(result))
1238
1547
  return;
1239
1548
  const events = result.events;
@@ -1293,16 +1602,28 @@ export class MessageDeliveryEngine {
1293
1602
  }
1294
1603
  if (eventObjects.length > 0 && ackContig > 0 && ackContig !== pageContigBefore) {
1295
1604
  const ackSeq = this.clampAckSeq('group.ack_events', 'event_seq', ns, ackContig);
1296
- client._transport.call('group.ack_events', {
1297
- group_id: groupId,
1298
- event_seq: ackSeq,
1299
- device_id: client._deviceId,
1300
- slot_id: client._slotId,
1301
- }, undefined, undefined, true).catch((e) => {
1605
+ try {
1606
+ const ackPromise = client._transport.call('group.ack_events', {
1607
+ group_id: groupId,
1608
+ event_seq: ackSeq,
1609
+ device_id: client._deviceId,
1610
+ slot_id: client._slotId,
1611
+ }, undefined, undefined, true);
1612
+ if (singlePage)
1613
+ await ackPromise;
1614
+ else
1615
+ ackPromise.catch((e) => {
1616
+ client._clientLog.debug(`group event auto-ack failed: group=${groupId} ${formatDeliveryError(e)}`);
1617
+ });
1618
+ }
1619
+ catch (e) {
1302
1620
  client._clientLog.debug(`group event auto-ack failed: group=${groupId} ${formatDeliveryError(e)}`);
1303
- });
1621
+ }
1304
1622
  }
1305
1623
  const nextAfter = Math.max(eventSeqs.length > 0 ? Math.max(...eventSeqs) : nextAfterSeq, nextAfterSeq);
1624
+ if (singlePage && result.has_more === true && nextAfter > nextAfterSeq) {
1625
+ continuationAfterSeq = nextAfter;
1626
+ }
1306
1627
  if (eventObjects.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
1307
1628
  break;
1308
1629
  nextAfterSeq = nextAfter;
@@ -1317,12 +1638,32 @@ export class MessageDeliveryEngine {
1317
1638
  }
1318
1639
  finally {
1319
1640
  client._gapFillDone.delete(dedupKey);
1320
- client._releasePullGate(ns, token);
1321
- if (filled > 0 && client._seqTracker.getContiguousSeq(ns) > afterSeq) {
1641
+ if (legacyGateToken !== null && typeof client._releasePullGate === 'function') {
1642
+ client._releasePullGate(ns, legacyGateToken);
1643
+ }
1644
+ if (singlePage && continuationAfterSeq > afterSeq) {
1645
+ this.enqueueOnlineUnreadEventHint({
1646
+ group_id: groupId,
1647
+ _online_hint_after_event_seq: continuationAfterSeq,
1648
+ });
1649
+ }
1650
+ else if (!singlePage && filled > 0 && client._seqTracker.getContiguousSeq(ns) > afterSeq) {
1322
1651
  void this.fillGroupEventGap(groupId);
1323
1652
  }
1324
1653
  }
1325
1654
  }
1655
+ enqueueOnlineUnreadEventHint(data) {
1656
+ const client = this.runtime.client;
1657
+ const groupId = String(data.group_id ?? '').trim();
1658
+ if (!groupId || client._sessionOptions?.background_sync === false)
1659
+ return;
1660
+ const rawAfter = Number(data._online_hint_after_event_seq ?? 0);
1661
+ const afterSeq = Number.isSafeInteger(rawAfter) && rawAfter > 0 ? rawAfter : 0;
1662
+ client._safeAsync(this.fillGroupEventGap(groupId, {
1663
+ afterSeq,
1664
+ singlePage: true,
1665
+ }));
1666
+ }
1326
1667
  async handleGroupChangedEventSeq(data, groupId) {
1327
1668
  const client = this.runtime.client;
1328
1669
  groupId = String(groupId ?? '').trim();
@@ -1339,10 +1680,8 @@ export class MessageDeliveryEngine {
1339
1680
  client._clientLog.debug(`group.changed online unread hint skipped: group=${groupId} background_sync=false`);
1340
1681
  return;
1341
1682
  }
1342
- client._clientLog.debug(`group.changed online unread hint triggering gap fill: group=${groupId}, event_seq=${eventSeq}`);
1343
- this.fillGroupEventGap(groupId).catch((exc) => {
1344
- client._clientLog.warn(`background gap fill trigger failed: ${formatDeliveryError(exc)}`);
1345
- });
1683
+ client._clientLog.debug(`group.changed online unread hint triggering event Forward: group=${groupId}, event_seq=${eventSeq}`);
1684
+ this.enqueueOnlineUnreadEventHint(data);
1346
1685
  return;
1347
1686
  }
1348
1687
  if (this.isSelfJoinGroupChanged(data)) {
@@ -1606,16 +1945,49 @@ export class MessageDeliveryEngine {
1606
1945
  if (!this.pendingGroupInlineAcks?.size || !this.inlineGenerationIsCurrent(generation)
1607
1946
  || !Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
1608
1947
  return;
1609
- let actualAckSeq = requestedSeq;
1948
+ const actualAckSeq = this.resolveGroupPullAckSeq(result, requestedSeq);
1949
+ this.onGroupV2AckSucceeded(groupId, actualAckSeq, generation);
1950
+ }
1951
+ resolveGroupPullAckSeq(result, requestedSeq) {
1952
+ if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
1953
+ return 0;
1610
1954
  const response = isJsonObject(result) ? result : null;
1611
- const cursor = response && isJsonObject(response.cursor)
1612
- ? response.cursor
1613
- : null;
1614
- if (cursor && Object.prototype.hasOwnProperty.call(cursor, 'current_seq')) {
1615
- const raw = cursor.current_seq;
1616
- actualAckSeq = typeof raw === 'number' && Number.isSafeInteger(raw) && raw >= 0 ? raw : 0;
1955
+ if (!response || !Object.prototype.hasOwnProperty.call(response, 'cursor'))
1956
+ return 0;
1957
+ const cursor = response.cursor;
1958
+ const cursorObject = isJsonObject(cursor) ? cursor : null;
1959
+ if (!cursorObject || !Object.prototype.hasOwnProperty.call(cursorObject, 'current_seq'))
1960
+ return 0;
1961
+ const raw = cursorObject.current_seq;
1962
+ return typeof raw === 'number' && Number.isSafeInteger(raw) && raw >= 0 ? raw : 0;
1963
+ }
1964
+ resolveForwardAckSeq(result, requestedSeq) {
1965
+ if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
1966
+ return 0;
1967
+ const response = isJsonObject(result) ? result : null;
1968
+ // 旧服务端成功响应可能不带累计水位字段;RPC 已成功,兼容按请求水位处理。
1969
+ if (!response)
1970
+ return requestedSeq;
1971
+ let raw;
1972
+ if (Object.prototype.hasOwnProperty.call(response, 'effective_ack_seq'))
1973
+ raw = response.effective_ack_seq;
1974
+ else if (Object.prototype.hasOwnProperty.call(response, 'ack_seq'))
1975
+ raw = response.ack_seq;
1976
+ else if (Object.prototype.hasOwnProperty.call(response, 'cursor')) {
1977
+ const cursor = response.cursor;
1978
+ const cursorObject = isJsonObject(cursor) ? cursor : null;
1979
+ if (cursorObject) {
1980
+ if (!Object.prototype.hasOwnProperty.call(cursorObject, 'current_seq'))
1981
+ return 0;
1982
+ raw = cursorObject.current_seq;
1983
+ }
1984
+ else {
1985
+ raw = cursor;
1986
+ }
1617
1987
  }
1618
- this.onGroupV2AckSucceeded(groupId, actualAckSeq, generation);
1988
+ else
1989
+ return requestedSeq;
1990
+ return typeof raw === 'number' && Number.isSafeInteger(raw) && raw >= 0 ? raw : 0;
1619
1991
  }
1620
1992
  resolveActualAckSeq(result, requestedSeq) {
1621
1993
  if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
@@ -1649,22 +2021,31 @@ export class MessageDeliveryEngine {
1649
2021
  const before = this.syncState(ns);
1650
2022
  const order = forceTail ? ['tail'] : typeof client._seqTracker.planRealtimeSync === 'function'
1651
2023
  ? client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit)
1652
- : (pushSeq > before.ack ? ['tail'] : []);
2024
+ : (() => {
2025
+ const gap = pushSeq - before.ack;
2026
+ if (gap <= 0)
2027
+ return before.ack < before.tail - 1 ? ['forward'] : [];
2028
+ return gap <= pageLimit * 2 ? ['forward'] : ['tail'];
2029
+ })();
2030
+ // Planner 依据 S-A 选路;H 已覆盖 S 也不代表 A 已连续提交,仍需前台 Forward。
2031
+ const headForward = order.length === 1 && order[0] === 'forward' && pushSeq > before.ack;
1653
2032
  let result = { messages: [], raw_count: 0 };
1654
2033
  let tailHead = null;
1655
- const run = async (request, background) => {
2034
+ const run = async (request, background, forceForward = false) => {
1656
2035
  const invoke = async () => {
1657
2036
  const live = this.syncState(ns);
1658
2037
  if (request.window_mode === 'tail' && !forceTail && typeof client._seqTracker.getSyncState === 'function' && pushSeq <= live.head)
1659
2038
  return { messages: [], raw_count: 0 };
1660
- if (request.window_mode !== 'tail' && live.ack >= live.tail - 1)
2039
+ if (request.window_mode !== 'tail' && !forceForward && !headForward && live.ack >= live.tail - 1)
2040
+ return { messages: [], raw_count: 0 };
2041
+ if (request.window_mode !== 'tail' && headForward && live.ack >= pushSeq)
1661
2042
  return { messages: [], raw_count: 0 };
1662
2043
  if (request.window_mode === 'tail') {
1663
2044
  const tailResult = await client._v2E2EE.pullV2TailInternal({
1664
2045
  window_mode: 'tail',
1665
2046
  after_seq: live.ack,
1666
2047
  limit: pageLimit,
1667
- });
2048
+ }, true);
1668
2049
  tailHead = this.syncState(ns).head;
1669
2050
  return tailResult;
1670
2051
  }
@@ -1686,8 +2067,24 @@ export class MessageDeliveryEngine {
1686
2067
  const state = this.syncState(ns);
1687
2068
  const needsForward = state.ack < state.tail - 1;
1688
2069
  const backgroundEnabled = client._sessionOptions?.background_sync !== false;
1689
- if (backgroundEnabled && (order.length === 1 && order[0] === 'forward' || (order.length === 1 && order[0] === 'tail' && needsForward))) {
1690
- result = await run({ after_seq: state.ack, limit: pageLimit, ...(order[0] === 'tail' ? { max_pages: 1 } : {}) }, true);
2070
+ // Tail 期间到达的新 Push 可能只更新 maxSeen 而没有扩展 Tail 窗口。
2071
+ const maxSeen = Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? 0);
2072
+ const tailMissedDuringPull = tailHead !== null && maxSeen > state.head;
2073
+ const tailForward = tailHead !== null && (needsForward || tailMissedDuringPull);
2074
+ const backgroundWindowForward = (order.length === 1 && order[0] === 'forward' && !headForward)
2075
+ || (order.length === 1 && order[0] === 'tail' && tailHead === null && needsForward);
2076
+ const foregroundForward = headForward || tailForward;
2077
+ if (foregroundForward || backgroundEnabled && backgroundWindowForward) {
2078
+ const forwardTarget = Math.max(headForward ? pushSeq : 0, tailForward || backgroundWindowForward ? state.tail - 1 : 0, tailMissedDuringPull ? maxSeen : 0);
2079
+ const requiredPages = Math.max(1, Math.ceil(Math.max(0, forwardTarget - state.ack) / pageLimit));
2080
+ result = await run({
2081
+ after_seq: state.ack,
2082
+ limit: pageLimit,
2083
+ ...(headForward ? { max_pages: Math.max(2, requiredPages) } : order[0] === 'tail' ? { max_pages: requiredPages } : {}),
2084
+ }, !foregroundForward, headForward || tailMissedDuringPull);
2085
+ }
2086
+ if (tailHead !== null && this.syncState(ns).ack < this.syncState(ns).tail - 1) {
2087
+ this.scheduleRealtimeWindowForward(ns);
1691
2088
  }
1692
2089
  if (tailHead !== null) {
1693
2090
  this.markRealtimeTailCompleted(result, tailHead);
@@ -1699,15 +2096,24 @@ export class MessageDeliveryEngine {
1699
2096
  const before = this.syncState(ns);
1700
2097
  const order = forceTail ? ['tail'] : typeof client._seqTracker.planRealtimeSync === 'function'
1701
2098
  ? client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit)
1702
- : (pushSeq > before.ack ? ['tail'] : []);
2099
+ : (() => {
2100
+ const gap = pushSeq - before.ack;
2101
+ if (gap <= 0)
2102
+ return before.ack < before.tail - 1 ? ['forward'] : [];
2103
+ return gap <= pageLimit * 2 ? ['forward'] : ['tail'];
2104
+ })();
2105
+ // Planner 依据 S-A 选路;H 已覆盖 S 也不代表 A 已连续提交,仍需前台 Forward。
2106
+ const headForward = order.length === 1 && order[0] === 'forward' && pushSeq > before.ack;
1703
2107
  let result = { messages: [], raw_count: 0 };
1704
2108
  let tailHead = null;
1705
- const run = async (request, background) => {
2109
+ const run = async (request, background, forceForward = false) => {
1706
2110
  const invoke = async () => {
1707
2111
  const live = this.syncState(ns);
1708
2112
  if (request.window_mode === 'tail' && !forceTail && typeof client._seqTracker.getSyncState === 'function' && pushSeq <= live.head)
1709
2113
  return { messages: [], raw_count: 0 };
1710
- if (request.window_mode !== 'tail' && live.ack >= live.tail - 1)
2114
+ if (request.window_mode !== 'tail' && !forceForward && !headForward && live.ack >= live.tail - 1)
2115
+ return { messages: [], raw_count: 0 };
2116
+ if (request.window_mode !== 'tail' && headForward && live.ack >= pushSeq)
1711
2117
  return { messages: [], raw_count: 0 };
1712
2118
  if (request.window_mode === 'tail') {
1713
2119
  const tailResult = await client._v2E2EE.pullGroupV2TailInternal({
@@ -1715,7 +2121,7 @@ export class MessageDeliveryEngine {
1715
2121
  window_mode: 'tail',
1716
2122
  after_seq: live.ack,
1717
2123
  limit: pageLimit,
1718
- });
2124
+ }, true);
1719
2125
  tailHead = this.syncState(ns).head;
1720
2126
  return tailResult;
1721
2127
  }
@@ -1736,15 +2142,30 @@ export class MessageDeliveryEngine {
1736
2142
  const state = this.syncState(ns);
1737
2143
  const needsForward = state.ack < state.tail - 1;
1738
2144
  const backgroundEnabled = client._sessionOptions?.background_sync !== false;
1739
- if (backgroundEnabled && (order.length === 1 && order[0] === 'forward' || (order.length === 1 && order[0] === 'tail' && needsForward))) {
1740
- result = await run({ after_seq: state.ack, limit: pageLimit, ...(order[0] === 'tail' ? { max_pages: 1 } : {}) }, true);
2145
+ const maxSeen = Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? 0);
2146
+ const tailMissedDuringPull = tailHead !== null && maxSeen > state.head;
2147
+ const tailForward = tailHead !== null && (needsForward || tailMissedDuringPull);
2148
+ const backgroundWindowForward = (order.length === 1 && order[0] === 'forward' && !headForward)
2149
+ || (order.length === 1 && order[0] === 'tail' && tailHead === null && needsForward);
2150
+ const foregroundForward = headForward || tailForward;
2151
+ if (foregroundForward || backgroundEnabled && backgroundWindowForward) {
2152
+ const forwardTarget = Math.max(headForward ? pushSeq : 0, tailForward || backgroundWindowForward ? state.tail - 1 : 0, tailMissedDuringPull ? maxSeen : 0);
2153
+ const requiredPages = Math.max(1, Math.ceil(Math.max(0, forwardTarget - state.ack) / pageLimit));
2154
+ result = await run({
2155
+ after_seq: state.ack,
2156
+ limit: pageLimit,
2157
+ ...(headForward ? { max_pages: Math.max(2, requiredPages) } : order[0] === 'tail' ? { max_pages: requiredPages } : {}),
2158
+ }, !foregroundForward, headForward || tailMissedDuringPull);
2159
+ }
2160
+ if (tailHead !== null && this.syncState(ns).ack < this.syncState(ns).tail - 1) {
2161
+ this.scheduleRealtimeWindowForward(ns, groupId);
1741
2162
  }
1742
2163
  if (tailHead !== null) {
1743
2164
  this.markRealtimeTailCompleted(result, tailHead);
1744
2165
  }
1745
2166
  return result;
1746
2167
  }
1747
- async runP2PForwardRecovery(ns, pageLimit) {
2168
+ async runP2PForwardRecovery(ns, pageLimit, background = false) {
1748
2169
  const client = this.runtime.client;
1749
2170
  const request = { after_seq: this.syncState(ns).ack, limit: pageLimit, max_pages: 1 };
1750
2171
  const invoke = async () => {
@@ -1755,20 +2176,22 @@ export class MessageDeliveryEngine {
1755
2176
  if (!client._rpcPipeline)
1756
2177
  return await invoke();
1757
2178
  const key = client._rpcPipeline.pullGateKeyForCall('message.pull', request);
1758
- return await client._rpcPipeline.runPullSerialized(key, invoke, true);
2179
+ return await client._rpcPipeline.runPullSerialized(key, invoke, background);
1759
2180
  }
1760
- async runGroupForwardRecovery(groupId, ns, pageLimit) {
2181
+ async runGroupForwardRecovery(groupId, ns, pageLimit, background = false, throughSeq = 0) {
1761
2182
  const client = this.runtime.client;
1762
- const request = { group_id: groupId, after_seq: this.syncState(ns).ack, limit: pageLimit, max_pages: 1 };
2183
+ const ack = this.syncState(ns).ack;
2184
+ const maxPages = Math.max(1, Math.ceil(Math.max(0, throughSeq - ack) / pageLimit));
2185
+ const request = { group_id: groupId, after_seq: ack, limit: pageLimit, max_pages: maxPages };
1763
2186
  const invoke = async () => {
1764
2187
  const after = this.syncState(ns).ack;
1765
- const messages = await client._pullGroupV2(groupId, after, pageLimit, { gateLocked: true, maxPages: 1 });
2188
+ const messages = await client._pullGroupV2(groupId, after, pageLimit, { gateLocked: true, maxPages });
1766
2189
  return { messages, raw_count: messages.length };
1767
2190
  };
1768
2191
  if (!client._rpcPipeline)
1769
2192
  return await invoke();
1770
2193
  const key = client._rpcPipeline.pullGateKeyForCall('group.pull', request);
1771
- return await client._rpcPipeline.runPullSerialized(key, invoke, true);
2194
+ return await client._rpcPipeline.runPullSerialized(key, invoke, background);
1772
2195
  }
1773
2196
  syncState(ns) {
1774
2197
  const tracker = this.runtime.client._seqTracker;
@@ -2131,7 +2554,7 @@ export class MessageDeliveryEngine {
2131
2554
  }
2132
2555
  if (!this.inlineGenerationIsCurrent(generation))
2133
2556
  return false;
2134
- client._commitSeqTrackerState(ns);
2557
+ await client._commitSeqTrackerState(ns);
2135
2558
  if (!this.inlineGenerationIsCurrent(generation))
2136
2559
  return false;
2137
2560
  this.markPublishedSeq(ns, seq);
@@ -2306,13 +2729,18 @@ export class MessageDeliveryEngine {
2306
2729
  if (!this.pendingP2PInlineAcks?.size || !this.inlineGenerationIsCurrent(generation)
2307
2730
  || !Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
2308
2731
  return;
2309
- let actualAckSeq = requestedSeq;
2732
+ const actualAckSeq = this.resolveP2PPullAckSeq(result, requestedSeq);
2733
+ this.onP2PV2AckSucceeded(actualAckSeq, generation);
2734
+ }
2735
+ resolveP2PPullAckSeq(result, requestedSeq) {
2736
+ if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
2737
+ return 0;
2310
2738
  const response = isJsonObject(result) ? result : null;
2311
2739
  if (response && Object.prototype.hasOwnProperty.call(response, 'server_ack_seq')) {
2312
2740
  const raw = response.server_ack_seq;
2313
- actualAckSeq = typeof raw === 'number' && Number.isSafeInteger(raw) && raw >= 0 ? raw : 0;
2741
+ return typeof raw === 'number' && Number.isSafeInteger(raw) && raw >= 0 ? raw : 0;
2314
2742
  }
2315
- this.onP2PV2AckSucceeded(actualAckSeq, generation);
2743
+ return 0;
2316
2744
  }
2317
2745
  async onV2PushNotification(data, expectedVersion, inlineGeneration) {
2318
2746
  const client = this.runtime.client;
@@ -2325,10 +2753,16 @@ export class MessageDeliveryEngine {
2325
2753
  const ns = client._aid ? `p2p:${client._aid}` : '';
2326
2754
  if (!pushData || !ns)
2327
2755
  return;
2756
+ if (String(pushData.kind ?? '').trim() === 'message.online_unread_hint') {
2757
+ if (client._sessionOptions?.background_sync === false)
2758
+ return;
2759
+ this.enqueueOnlineUnreadP2pHint(pushData);
2760
+ return;
2761
+ }
2328
2762
  const inlinePresent = Object.prototype.hasOwnProperty.call(pushData, 'inline_message');
2329
2763
  const pushSeq = this.realtimeHeadSeq(rawPushSeq, inlinePresent);
2330
2764
  if (pushSeq === null) {
2331
- if (inlinePresent && rawPushSeq !== undefined && rawPushSeq !== null) {
2765
+ if (inlinePresent) {
2332
2766
  await this.recoverInvalidP2PRealtimeHead(ns, rawPushSeq);
2333
2767
  }
2334
2768
  return;
@@ -2350,34 +2784,26 @@ export class MessageDeliveryEngine {
2350
2784
  catch (exc) {
2351
2785
  if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
2352
2786
  return;
2353
- client._clientLog.warn(`V2 P2P inline push rejected, falling back to Tail: seq=${pushSeq} err=${formatDeliveryError(exc)}`);
2787
+ client._clientLog.warn(`V2 P2P inline push rejected, falling back to sync planner: seq=${pushSeq} err=${formatDeliveryError(exc)}`);
2354
2788
  }
2355
2789
  const before = this.syncState(ns);
2356
2790
  client._seqTracker.updateMaxSeen(ns, pushSeq);
2357
- const hasGap = before.ack < before.tail - 1;
2358
- if (pushSeq <= before.head && !hasGap && !inlinePresent) {
2791
+ if (pushSeq <= before.ack && before.ack >= before.tail - 1) {
2359
2792
  client._clientLog.debug(`V2 P2P duplicate push ignored: seq=${pushSeq} A/T/H=${JSON.stringify(before)}`);
2360
2793
  this.retryPendingP2PInlineAck(ns, pushSeq, 'covered inline push');
2361
2794
  return;
2362
2795
  }
2363
- if (!this.beginRealtimeSync(ns)) {
2364
- client._clientLog.debug(`V2 P2P push coalesced into in-flight realtime sync: ns=${ns} seq=${pushSeq}`);
2796
+ const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
2797
+ if (pullActive || !this.beginRealtimeSync(ns)) {
2798
+ client._recordPendingP2pPull(ns, pushSeq);
2799
+ client._clientLog.debug(`V2 P2P push queued behind in-flight realtime sync: ns=${ns} seq=${pushSeq}`);
2365
2800
  return;
2366
2801
  }
2367
2802
  try {
2368
- const syncResult = await this.runP2PRealtimeSync(ns, pushSeq, 50, inlinePresent && !inlineConsumed);
2803
+ await this.runP2PRealtimeSync(ns, pushSeq, 50, false);
2369
2804
  if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
2370
2805
  return;
2371
- const tailHead = this.realtimeTailHead(syncResult);
2372
- const maxSeen = Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? 0);
2373
- if (client._sessionOptions?.background_sync !== false
2374
- && tailHead !== null && Number.isSafeInteger(maxSeen) && maxSeen > tailHead) {
2375
- await this.runP2PForwardRecovery(ns, 50).catch((exc) => {
2376
- client._clientLog.warn(`V2 P2P stale Tail Forward recovery failed: ${formatDeliveryError(exc)}`);
2377
- });
2378
- if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
2379
- return;
2380
- }
2806
+ // Tail 期间新到达的 Push 已由 realtime sync 在同一周期内补至多一页 Forward。
2381
2807
  }
2382
2808
  catch (exc) {
2383
2809
  client._clientLog.warn(`V2 P2P push auto-sync failed: ${formatDeliveryError(exc)}`);
@@ -2385,6 +2811,7 @@ export class MessageDeliveryEngine {
2385
2811
  finally {
2386
2812
  if (inlineGeneration === undefined || this.inlineGenerationIsCurrent(inlineGeneration)) {
2387
2813
  this.endRealtimeSync(ns);
2814
+ client._schedulePendingP2pPullIfNeeded?.(ns, 'push-auto-pull-complete');
2388
2815
  }
2389
2816
  }
2390
2817
  }
@@ -2393,50 +2820,35 @@ export class MessageDeliveryEngine {
2393
2820
  const groupId = String(data.group_id ?? '').trim();
2394
2821
  if (!groupId)
2395
2822
  return;
2396
- client._onlineUnreadHintQueue.set(groupId, { ...data });
2397
- if (client._onlineUnreadHintTimer || client._onlineUnreadHintDrainActive)
2398
- return;
2399
- const delayMs = Math.max(0, Number(client._onlineUnreadHintInitialDelayMs ?? 750) || 0);
2400
- this.runtime.delivery.setOnlineUnreadHintTimer(setTimeout(() => {
2401
- this.runtime.delivery.setOnlineUnreadHintTimer(null);
2402
- client._safeAsync(this.drainOnlineUnreadHints());
2403
- }, delayMs));
2404
- }
2405
- async drainOnlineUnreadHints() {
2406
- const client = this.runtime.client;
2407
- if (client._onlineUnreadHintDrainActive)
2408
- return;
2409
- this.runtime.delivery.setOnlineUnreadHintDrainActive(true);
2410
- const attemptedAckByGroup = new Map();
2411
- try {
2412
- while (client._onlineUnreadHintQueue.size > 0) {
2413
- if (client.state !== ConnectionState.READY)
2414
- return;
2415
- const groupId = client._onlineUnreadHintQueue.keys().next().value;
2416
- if (!groupId)
2417
- return;
2418
- const payload = client._onlineUnreadHintQueue.get(groupId);
2419
- client._onlineUnreadHintQueue.delete(groupId);
2420
- if (!payload)
2421
- continue;
2422
- const currentAck = this.syncState(`group:${groupId}`).ack;
2423
- const attemptedAck = attemptedAckByGroup.get(groupId);
2424
- if (attemptedAck !== undefined && currentAck <= attemptedAck)
2425
- continue;
2426
- attemptedAckByGroup.set(groupId, currentAck);
2427
- await this.onRawGroupV2MessageCreated({ ...payload, _online_hint_drained: true });
2428
- const intervalMs = Math.max(0, Number(client._onlineUnreadHintIntervalMs ?? 50) || 0);
2429
- if (intervalMs > 0 && client._onlineUnreadHintQueue.size > 0) {
2430
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
2823
+ const ns = `group:${groupId}`;
2824
+ const targets = this.onlineUnreadHintTargets ?? new Map();
2825
+ const tasks = this.onlineUnreadHintTasks ?? new Map();
2826
+ this.onlineUnreadHintTargets = targets;
2827
+ this.onlineUnreadHintTasks = tasks;
2828
+ targets.set(ns, Math.max(targets.get(ns) ?? 0, positiveSafeSequenceHint(data.seq)));
2829
+ if (tasks.has(ns))
2830
+ return;
2831
+ const task = Promise.resolve().then(async () => {
2832
+ try {
2833
+ while (true) {
2834
+ const ackBefore = this.syncState(ns).ack;
2835
+ await this.runGroupForwardRecovery(groupId, ns, 50, true, targets.get(ns) ?? 0);
2836
+ const ackAfter = this.syncState(ns).ack;
2837
+ if (ackAfter >= (targets.get(ns) ?? 0) || ackAfter <= ackBefore)
2838
+ return;
2431
2839
  }
2432
2840
  }
2433
- }
2434
- catch (exc) {
2435
- client._clientLog.debug(`online unread hint drain failed: ${formatDeliveryError(exc)}`);
2436
- }
2437
- finally {
2438
- this.runtime.delivery.setOnlineUnreadHintDrainActive(false);
2439
- }
2841
+ catch (exc) {
2842
+ client._clientLog.debug(`online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
2843
+ }
2844
+ finally {
2845
+ targets.delete(ns);
2846
+ if (tasks.get(ns) === task)
2847
+ tasks.delete(ns);
2848
+ }
2849
+ });
2850
+ tasks.set(ns, task);
2851
+ client._safeAsync(task);
2440
2852
  }
2441
2853
  async onRawGroupV2MessageCreated(data, expectedVersion, inlineGeneration) {
2442
2854
  const client = this.runtime.client;
@@ -2457,29 +2869,16 @@ export class MessageDeliveryEngine {
2457
2869
  return;
2458
2870
  }
2459
2871
  if (seq === null) {
2460
- if (inlinePresent && rawSeq !== undefined && rawSeq !== null) {
2872
+ if (inlinePresent) {
2461
2873
  await this.recoverInvalidGroupRealtimeHead(groupId, `group:${groupId}`, rawSeq);
2462
2874
  }
2463
2875
  return;
2464
2876
  }
2465
2877
  const eventKind = String(d.kind ?? '').trim();
2466
2878
  if (eventKind === 'group.online_unread_hint') {
2467
- if (!d._online_hint_drained) {
2468
- if (client._sessionOptions.background_sync === false)
2469
- return;
2470
- this.enqueueOnlineUnreadHint(d);
2471
- return;
2472
- }
2473
- // Hint 不是真实消息 Head:不消费 inline、不推进水位、也不直接 ACK。
2474
- // 只在后台同步开启时,从已提交 Ack 强制做一次普通 Forward,绝不进入 Tail planner。
2475
2879
  if (client._sessionOptions.background_sync === false)
2476
2880
  return;
2477
- try {
2478
- await this.runGroupForwardRecovery(groupId, `group:${groupId}`, 50);
2479
- }
2480
- catch (exc) {
2481
- client._clientLog.warn(`group online unread hint recovery pull failed: group=${groupId} err=${formatDeliveryError(exc)}`);
2482
- }
2881
+ this.enqueueOnlineUnreadHint(d);
2483
2882
  return;
2484
2883
  }
2485
2884
  const ns = `group:${groupId}`;
@@ -2500,40 +2899,32 @@ export class MessageDeliveryEngine {
2500
2899
  catch (exc) {
2501
2900
  if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
2502
2901
  return;
2503
- client._clientLog.warn(`V2 group inline push rejected, falling back to Tail: group=${groupId} seq=${seq} err=${formatDeliveryError(exc)}`);
2902
+ client._clientLog.warn(`V2 group inline push rejected, falling back to sync planner: group=${groupId} seq=${seq} err=${formatDeliveryError(exc)}`);
2504
2903
  }
2505
2904
  }
2506
2905
  const before = this.syncState(ns);
2507
2906
  client._seqTracker.updateMaxSeen(ns, seq);
2508
- const hasGap = before.ack < before.tail - 1;
2509
- if (seq <= before.head && !hasGap && !inlinePresent) {
2907
+ if (seq <= before.ack && before.ack >= before.tail - 1) {
2510
2908
  client._clientLog.debug(`V2 group duplicate push ignored: group=${groupId} seq=${seq} A/T/H=${JSON.stringify(before)}`);
2511
2909
  const pending = this.pendingGroupInlineAcks?.get(ns);
2512
2910
  if (pending?.seq === seq) {
2513
2911
  this.retryPendingGroupInlineAck(ns, seq, 'covered inline push');
2514
2912
  }
2515
- else {
2913
+ else if (!inlinePresent) {
2516
2914
  this.fireGroupV2Ack(groupId, seq, 'covered push');
2517
2915
  }
2518
2916
  return;
2519
2917
  }
2520
- if (!this.beginRealtimeSync(ns)) {
2521
- client._clientLog.debug(`V2 group push coalesced into in-flight realtime sync: ns=${ns} seq=${seq}`);
2918
+ const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
2919
+ if (pullActive || !this.beginRealtimeSync(ns)) {
2920
+ this.recordPendingGroupPull(ns, seq);
2921
+ client._clientLog.debug(`V2 group push queued behind in-flight realtime sync: ns=${ns} seq=${seq}`);
2522
2922
  return;
2523
2923
  }
2524
- const pullTask = this.runGroupRealtimeSync(groupId, ns, seq, 50, inlinePresent && !inlineConsumed).then(async (syncResult) => {
2924
+ void this.runGroupRealtimeSync(groupId, ns, seq, 50, false).then(async () => {
2525
2925
  if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
2526
2926
  return;
2527
- const tailHead = this.realtimeTailHead(syncResult);
2528
- const maxSeen = Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? 0);
2529
- if (client._sessionOptions?.background_sync !== false
2530
- && tailHead !== null && Number.isSafeInteger(maxSeen) && maxSeen > tailHead) {
2531
- await this.runGroupForwardRecovery(groupId, ns, 50).catch((exc) => {
2532
- client._clientLog.warn(`V2 group stale Tail Forward recovery failed: group=${groupId} err=${formatDeliveryError(exc)}`);
2533
- });
2534
- if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
2535
- return;
2536
- }
2927
+ // Tail 期间新到达的 Push 已由 realtime sync 在同一周期内补至多一页 Forward。
2537
2928
  }).catch((exc) => {
2538
2929
  if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
2539
2930
  return;
@@ -2541,11 +2932,9 @@ export class MessageDeliveryEngine {
2541
2932
  }).finally(() => {
2542
2933
  if (inlineGeneration === undefined || this.inlineGenerationIsCurrent(inlineGeneration)) {
2543
2934
  this.endRealtimeSync(ns);
2935
+ this.schedulePendingGroupPullIfNeeded(ns, 'push-auto-pull-complete');
2544
2936
  }
2545
2937
  });
2546
- if (d._online_hint_drained) {
2547
- await pullTask;
2548
- }
2549
2938
  }
2550
2939
  restoreSeqTrackerState() {
2551
2940
  const client = this.runtime.client;
@@ -2598,11 +2987,20 @@ export class MessageDeliveryEngine {
2598
2987
  migrateSeqStateGroupIds(state) {
2599
2988
  return state;
2600
2989
  }
2601
- saveSeqTrackerState() {
2990
+ enqueueOnlineUnreadP2pHint(data) {
2991
+ const client = this.runtime.client;
2992
+ if (!client._aid || !client._v2Session)
2993
+ return;
2994
+ const ns = `p2p:${client._aid}`;
2995
+ client._safeAsync(this.runP2PForwardRecovery(ns, 50, true).catch((exc) => {
2996
+ client._clientLog.debug(`P2P online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
2997
+ }));
2998
+ }
2999
+ async saveSeqTrackerState() {
2602
3000
  const client = this.runtime.client;
2603
3001
  const context = this.currentSeqTrackerContext();
2604
3002
  if (!context) {
2605
- this.flushSeqTrackerPending();
3003
+ await this.flushSeqTrackerPending();
2606
3004
  return;
2607
3005
  }
2608
3006
  const state = typeof client._seqTracker.exportSyncState === 'function'
@@ -2620,13 +3018,13 @@ export class MessageDeliveryEngine {
2620
3018
  }
2621
3019
  if (Object.keys(state).length > 0)
2622
3020
  this.mergeSeqTrackerPending(context, state);
2623
- this.flushSeqTrackerPending();
3021
+ await this.flushSeqTrackerPending();
2624
3022
  }
2625
3023
  persistSeq(ns, forceSeq) {
2626
3024
  const client = this.runtime.client;
2627
3025
  const context = this.currentSeqTrackerContext();
2628
3026
  if (!context) {
2629
- this.flushSeqTrackerPending();
3027
+ void this.flushSeqTrackerPending();
2630
3028
  return;
2631
3029
  }
2632
3030
  const seq = forceSeq ?? client._seqTracker.getContiguousSeq(ns);
@@ -2641,7 +3039,7 @@ export class MessageDeliveryEngine {
2641
3039
  this.mergeSeqTrackerPending(context, { [ns]: state });
2642
3040
  this.scheduleSeqTrackerFlush();
2643
3041
  }
2644
- commitSeqTrackerState(ns) {
3042
+ async commitSeqTrackerState(ns) {
2645
3043
  const client = this.runtime.client;
2646
3044
  const context = this.currentSeqTrackerContext();
2647
3045
  if (!context || !ns)
@@ -2652,11 +3050,17 @@ export class MessageDeliveryEngine {
2652
3050
  if (![state.ack, state.tail, state.head].some((value) => Number(value) > 0))
2653
3051
  return;
2654
3052
  const [aid, deviceId, slotId] = JSON.parse(context);
2655
- this.writeSeqTrackerState(aid, deviceId, slotId, { [ns]: state }, true);
2656
- this.dropSeqTrackerPending(ns);
3053
+ const pendingBeforeCommit = this.takeSeqTrackerPending(context, ns);
3054
+ try {
3055
+ await this.writeSeqTrackerState(aid, deviceId, slotId, { [ns]: state }, true);
3056
+ }
3057
+ catch (exc) {
3058
+ this.restoreSeqTrackerPending(context, ns, pendingBeforeCommit);
3059
+ throw exc;
3060
+ }
2657
3061
  client._clientLog.debug(`seq_tracker atomic commit: ns=${ns} A/T/H=${JSON.stringify(state)} context=${context}`);
2658
3062
  }
2659
- flushSeqTrackerPending() {
3063
+ async flushSeqTrackerPending() {
2660
3064
  const client = this.runtime.client;
2661
3065
  this.cancelSeqTrackerFlushTimer();
2662
3066
  const context = client._seqTrackerPendingPersistContext;
@@ -2669,7 +3073,7 @@ export class MessageDeliveryEngine {
2669
3073
  const state = Object.fromEntries(pending.entries());
2670
3074
  pending.clear();
2671
3075
  client._seqTrackerPendingPersistContext = null;
2672
- this.writeSeqTrackerState(aid, deviceId, slotId, state);
3076
+ await this.writeSeqTrackerState(aid, deviceId, slotId, state);
2673
3077
  }
2674
3078
  dropSeqTrackerPending(ns) {
2675
3079
  const client = this.runtime.client;
@@ -2680,6 +3084,34 @@ export class MessageDeliveryEngine {
2680
3084
  this.cancelSeqTrackerFlushTimer();
2681
3085
  }
2682
3086
  }
3087
+ takeSeqTrackerPending(context, ns) {
3088
+ const client = this.runtime.client;
3089
+ if (client._seqTrackerPendingPersistContext !== context)
3090
+ return null;
3091
+ const pending = this.seqTrackerPending();
3092
+ const state = pending.get(ns);
3093
+ if (!state)
3094
+ return null;
3095
+ pending.delete(ns);
3096
+ if (pending.size === 0) {
3097
+ client._seqTrackerPendingPersistContext = null;
3098
+ this.cancelSeqTrackerFlushTimer();
3099
+ }
3100
+ return { ...state };
3101
+ }
3102
+ restoreSeqTrackerPending(context, ns, state) {
3103
+ if (!state || this.currentSeqTrackerContext() !== context)
3104
+ return;
3105
+ const client = this.runtime.client;
3106
+ const pendingContext = client._seqTrackerPendingPersistContext;
3107
+ if (pendingContext && pendingContext !== context)
3108
+ return;
3109
+ const pending = this.seqTrackerPending();
3110
+ if (!pending.has(ns))
3111
+ pending.set(ns, state);
3112
+ client._seqTrackerPendingPersistContext = context;
3113
+ this.scheduleSeqTrackerFlush();
3114
+ }
2683
3115
  currentSeqTrackerContext() {
2684
3116
  const client = this.runtime.client;
2685
3117
  if (!client._aid)
@@ -2698,7 +3130,7 @@ export class MessageDeliveryEngine {
2698
3130
  const pending = this.seqTrackerPending();
2699
3131
  const pendingContext = client._seqTrackerPendingPersistContext;
2700
3132
  if (pendingContext && pendingContext !== context && pending.size > 0) {
2701
- this.flushSeqTrackerPending();
3133
+ void this.flushSeqTrackerPending();
2702
3134
  }
2703
3135
  client._seqTrackerPendingPersistContext = context;
2704
3136
  for (const [ns, raw] of Object.entries(state)) {
@@ -2719,7 +3151,7 @@ export class MessageDeliveryEngine {
2719
3151
  return;
2720
3152
  client._seqTrackerFlushTimer = setTimeout(() => {
2721
3153
  client._seqTrackerFlushTimer = null;
2722
- this.flushSeqTrackerPending();
3154
+ void this.flushSeqTrackerPending();
2723
3155
  }, SEQ_TRACKER_PERSIST_FLUSH_DELAY_MS);
2724
3156
  }
2725
3157
  cancelSeqTrackerFlushTimer() {
@@ -2729,26 +3161,34 @@ export class MessageDeliveryEngine {
2729
3161
  client._seqTrackerFlushTimer = null;
2730
3162
  }
2731
3163
  }
2732
- writeSeqTrackerState(aid, deviceId, slotId, state, throwOnError = false) {
3164
+ async writeSeqTrackerState(aid, deviceId, slotId, state, throwOnError = false) {
3165
+ await this.enqueueSeqTrackerWrite(() => this.writeSeqTrackerStateNow(aid, deviceId, slotId, state, throwOnError));
3166
+ }
3167
+ enqueueSeqTrackerWrite(operation) {
3168
+ const run = this.seqTrackerWriteChain.then(operation, operation);
3169
+ this.seqTrackerWriteChain = run.catch(() => { });
3170
+ return run;
3171
+ }
3172
+ async writeSeqTrackerStateNow(aid, deviceId, slotId, state, throwOnError = false) {
2733
3173
  const client = this.runtime.client;
2734
3174
  if (!aid || Object.keys(state).length === 0)
2735
3175
  return;
2736
3176
  try {
2737
3177
  const saveSync = client._tokenStore.saveSyncStates;
2738
3178
  if (typeof saveSync === 'function') {
2739
- saveSync.call(client._tokenStore, aid, deviceId, slotId, state);
3179
+ await saveSync.call(client._tokenStore, aid, deviceId, slotId, state);
2740
3180
  return;
2741
3181
  }
2742
3182
  const saveFn = client._tokenStore.saveSeq;
2743
3183
  if (typeof saveFn === 'function') {
2744
3184
  for (const [ns, triplet] of Object.entries(state)) {
2745
- saveFn.call(client._tokenStore, aid, deviceId, slotId, ns, triplet.ack);
3185
+ await saveFn.call(client._tokenStore, aid, deviceId, slotId, ns, triplet.ack);
2746
3186
  }
2747
3187
  return;
2748
3188
  }
2749
3189
  const updater = client._tokenStore.updateInstanceState;
2750
3190
  if (typeof updater === 'function') {
2751
- updater.call(client._tokenStore, aid, deviceId, slotId, (metadata) => {
3191
+ await updater.call(client._tokenStore, aid, deviceId, slotId, (metadata) => {
2752
3192
  const existing = isJsonObject(metadata.seq_tracker_state)
2753
3193
  ? { ...metadata.seq_tracker_state }
2754
3194
  : {};
@@ -2775,36 +3215,56 @@ export class MessageDeliveryEngine {
2775
3215
  const client = this.runtime.client;
2776
3216
  if (!client._aid || !ns)
2777
3217
  return;
3218
+ const aid = client._aid;
3219
+ const deviceId = client._deviceId;
3220
+ const slotId = client._slotId;
2778
3221
  const seq = client._seqTracker.getContiguousSeq(ns);
2779
3222
  this.dropSeqTrackerPending(ns);
2780
- try {
2781
- if (seq > 0 && typeof client._tokenStore.saveSeq === 'function') {
2782
- client._tokenStore.saveSeq(client._aid, client._deviceId, client._slotId, ns, seq);
2783
- return;
2784
- }
2785
- const deleteSeq = client._tokenStore.deleteSeq;
2786
- if (seq <= 0 && typeof deleteSeq === 'function') {
2787
- deleteSeq.call(client._tokenStore, client._aid, client._deviceId, client._slotId, ns);
2788
- return;
2789
- }
2790
- const updater = client._tokenStore.updateInstanceState;
2791
- if (seq <= 0 && typeof updater === 'function') {
2792
- updater.call(client._tokenStore, client._aid, client._deviceId, client._slotId, (metadata) => {
2793
- if (isJsonObject(metadata.seq_tracker_state)) {
2794
- const next = { ...metadata.seq_tracker_state };
2795
- delete next[ns];
2796
- metadata.seq_tracker_state = next;
2797
- }
2798
- return metadata;
2799
- });
2800
- return;
2801
- }
2802
- if (seq > 0) {
2803
- this.saveSeqTrackerState();
2804
- }
3223
+ const saveSeq = client._tokenStore.saveSeq;
3224
+ if (seq > 0 && typeof saveSeq === 'function') {
3225
+ void this.enqueueSeqTrackerWrite(async () => {
3226
+ try {
3227
+ await saveSeq.call(client._tokenStore, aid, deviceId, slotId, ns, seq);
3228
+ }
3229
+ catch (exc) {
3230
+ client._clientLog.debug(`persist repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
3231
+ }
3232
+ });
3233
+ return;
2805
3234
  }
2806
- catch (exc) {
2807
- client._clientLog.debug(`persist repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
3235
+ const deleteSeq = client._tokenStore.deleteSeq;
3236
+ if (seq <= 0 && typeof deleteSeq === 'function') {
3237
+ void this.enqueueSeqTrackerWrite(async () => {
3238
+ try {
3239
+ await deleteSeq.call(client._tokenStore, aid, deviceId, slotId, ns);
3240
+ }
3241
+ catch (exc) {
3242
+ client._clientLog.debug(`delete repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
3243
+ }
3244
+ });
3245
+ return;
3246
+ }
3247
+ const updater = client._tokenStore.updateInstanceState;
3248
+ if (seq <= 0 && typeof updater === 'function') {
3249
+ void this.enqueueSeqTrackerWrite(async () => {
3250
+ try {
3251
+ await updater.call(client._tokenStore, aid, deviceId, slotId, (metadata) => {
3252
+ if (isJsonObject(metadata.seq_tracker_state)) {
3253
+ const next = { ...metadata.seq_tracker_state };
3254
+ delete next[ns];
3255
+ metadata.seq_tracker_state = next;
3256
+ }
3257
+ return metadata;
3258
+ });
3259
+ }
3260
+ catch (exc) {
3261
+ client._clientLog.debug(`delete repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
3262
+ }
3263
+ });
3264
+ return;
3265
+ }
3266
+ if (seq > 0) {
3267
+ void this.saveSeqTrackerState();
2808
3268
  }
2809
3269
  }
2810
3270
  clampAckSeq(method, field, ns, seq) {
@@ -2864,13 +3324,15 @@ export class MessageDeliveryEngine {
2864
3324
  }
2865
3325
  return params;
2866
3326
  }
2867
- async drainOrderedMessages(ns, beforeSeq, pullResponse = false) {
3327
+ async drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true) {
2868
3328
  const client = this.runtime.client;
3329
+ this.ensurePullOperationCurrent();
2869
3330
  const queue = client._pendingOrderedMsgs.get(ns);
2870
3331
  if (!queue || queue.size === 0)
2871
3332
  return;
2872
3333
  let delivered = false;
2873
3334
  while (true) {
3335
+ this.ensurePullOperationCurrent();
2874
3336
  const contig = client._seqTracker.getContiguousSeq(ns);
2875
3337
  const ready = [...queue.keys()]
2876
3338
  .filter((seq) => seq <= contig && (beforeSeq === undefined || seq < beforeSeq))
@@ -2896,6 +3358,7 @@ export class MessageDeliveryEngine {
2896
3358
  continue;
2897
3359
  }
2898
3360
  await this.publishOrderedQueueItem(ns, item.event, seq, item.payload, 'ordered-drain', pullResponse);
3361
+ this.ensurePullOperationCurrent();
2899
3362
  this.markPublishedSeq(ns, seq);
2900
3363
  client._markOrderedSeqDelivered(ns, seq);
2901
3364
  delivered = true;
@@ -2903,16 +3366,20 @@ export class MessageDeliveryEngine {
2903
3366
  }
2904
3367
  if (queue.size === 0) {
2905
3368
  client._pendingOrderedMsgs.delete(ns);
2906
- if (delivered)
2907
- this.saveSeqTrackerState();
3369
+ if (delivered && persist) {
3370
+ this.ensurePullOperationCurrent();
3371
+ await this.saveSeqTrackerState();
3372
+ }
2908
3373
  }
2909
3374
  }
2910
3375
  async publishOrderedMessage(event, ns, seq, payload) {
2911
3376
  const client = this.runtime.client;
3377
+ this.ensurePullOperationCurrent();
2912
3378
  const seqNum = Number(seq);
2913
3379
  if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0) {
2914
3380
  client._clientLog.debug(`publish ordered direct(no-seq): event=${event}, ns=${ns || '<none>'}, seq=${String(seq)}`);
2915
3381
  await this.publishOrderedQueueItem(ns, event, seqNum, payload, 'ordered');
3382
+ this.ensurePullOperationCurrent();
2916
3383
  return true;
2917
3384
  }
2918
3385
  if (client._pushedSeqs.get(ns)?.has(seqNum)) {
@@ -2930,6 +3397,7 @@ export class MessageDeliveryEngine {
2930
3397
  return false;
2931
3398
  }
2932
3399
  await this.drainOrderedMessages(ns, seqNum);
3400
+ this.ensurePullOperationCurrent();
2933
3401
  if (client._pushedSeqs.get(ns)?.has(seqNum)) {
2934
3402
  client._clientLog.debug(`publish ordered skipped after-drain duplicate: event=${event}, ns=${ns}, seq=${seqNum}`);
2935
3403
  return false;
@@ -2939,19 +3407,24 @@ export class MessageDeliveryEngine {
2939
3407
  if (queue && queue.size === 0)
2940
3408
  client._pendingOrderedMsgs.delete(ns);
2941
3409
  await this.publishOrderedQueueItem(ns, event, seqNum, payload, 'ordered');
3410
+ this.ensurePullOperationCurrent();
2942
3411
  this.markPublishedSeq(ns, seqNum);
2943
3412
  client._markOrderedSeqDelivered(ns, seqNum);
2944
3413
  client._clientLog.debug(`publish ordered delivered: event=${event}, ns=${ns}, seq=${seqNum}`);
2945
3414
  await this.drainOrderedMessages(ns);
2946
- if (!client._pendingOrderedMsgs.get(ns))
2947
- this.saveSeqTrackerState();
3415
+ if (!client._pendingOrderedMsgs.get(ns)) {
3416
+ this.ensurePullOperationCurrent();
3417
+ await this.saveSeqTrackerState();
3418
+ }
2948
3419
  return true;
2949
3420
  }
2950
3421
  async publishOrderedGroupRecall(ns, seq, message) {
2951
3422
  const client = this.runtime.client;
3423
+ this.ensurePullOperationCurrent();
2952
3424
  const seqNum = Number(seq);
2953
3425
  if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0) {
2954
3426
  await this.publishGroupRecallTombstone(ns.replace(/^group:/, ''), seq, message);
3427
+ this.ensurePullOperationCurrent();
2955
3428
  return true;
2956
3429
  }
2957
3430
  // 撤回 tombstone 也占 seq:推进 contiguous 并标记已发布,确保 ack 正常推进、不留空洞。
@@ -2961,24 +3434,31 @@ export class MessageDeliveryEngine {
2961
3434
  }
2962
3435
  client._seqTracker.onMessageSeq(ns, seqNum);
2963
3436
  await this.publishGroupRecallTombstone(ns.replace(/^group:/, ''), seq, message);
3437
+ this.ensurePullOperationCurrent();
2964
3438
  this.markPublishedSeq(ns, seqNum);
2965
3439
  client._markOrderedSeqDelivered?.(ns, seqNum);
2966
3440
  await this.drainOrderedMessages(ns);
2967
- if (!client._pendingOrderedMsgs.get(ns))
2968
- this.saveSeqTrackerState();
3441
+ if (!client._pendingOrderedMsgs.get(ns)) {
3442
+ this.ensurePullOperationCurrent();
3443
+ await this.saveSeqTrackerState();
3444
+ }
2969
3445
  return true;
2970
3446
  }
2971
- async publishPulledMessage(event, ns, seq, payload) {
3447
+ async publishPulledMessage(event, ns, seq, payload, persist = true) {
2972
3448
  const client = this.runtime.client;
3449
+ this.ensurePullOperationCurrent();
2973
3450
  const seqNum = Number(seq);
2974
3451
  if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0 || !ns) {
2975
3452
  client._clientLog.debug(`publish pulled direct(no-seq): event=${event}, ns=${ns || '<none>'}, seq=${String(seq)}`);
2976
3453
  if (event === 'message.recalled') {
2977
- return this.publishMessageRecallTombstone(seq, payload);
3454
+ const published = await this.publishMessageRecallTombstone(seq, payload);
3455
+ this.ensurePullOperationCurrent();
3456
+ return published;
2978
3457
  }
2979
3458
  const published = client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, 'pull'));
2980
3459
  if (isPromiseLike(published))
2981
3460
  await published;
3461
+ this.ensurePullOperationCurrent();
2982
3462
  return true;
2983
3463
  }
2984
3464
  const queue = client._pendingOrderedMsgs.get(ns);
@@ -2989,23 +3469,28 @@ export class MessageDeliveryEngine {
2989
3469
  client._pendingOrderedMsgs.delete(ns);
2990
3470
  return false;
2991
3471
  }
2992
- await this.drainOrderedMessages(ns, seqNum, true);
3472
+ await this.drainOrderedMessages(ns, seqNum, true, persist);
3473
+ this.ensurePullOperationCurrent();
2993
3474
  queue?.delete(seqNum);
2994
3475
  if (queue && queue.size === 0)
2995
3476
  client._pendingOrderedMsgs.delete(ns);
2996
3477
  if (event === 'message.recalled') {
2997
3478
  const recallPublished = await this.publishMessageRecallTombstone(seqNum, payload);
3479
+ this.ensurePullOperationCurrent();
2998
3480
  this.markPublishedSeq(ns, seqNum);
2999
3481
  client._clientLog.debug(`publish pulled delivered: event=${event}, ns=${ns}, seq=${seqNum}`);
3000
- await this.drainOrderedMessages(ns, undefined, true);
3482
+ await this.drainOrderedMessages(ns, undefined, true, persist);
3483
+ this.ensurePullOperationCurrent();
3001
3484
  return recallPublished;
3002
3485
  }
3003
3486
  const published = client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, 'pull'));
3004
3487
  if (isPromiseLike(published))
3005
3488
  await published;
3489
+ this.ensurePullOperationCurrent();
3006
3490
  this.markPublishedSeq(ns, seqNum);
3007
3491
  client._clientLog.debug(`publish pulled delivered: event=${event}, ns=${ns}, seq=${seqNum}`);
3008
- await this.drainOrderedMessages(ns, undefined, true);
3492
+ await this.drainOrderedMessages(ns, undefined, true, persist);
3493
+ this.ensurePullOperationCurrent();
3009
3494
  return true;
3010
3495
  }
3011
3496
  }