@agentunion/fastaun-browser 0.5.18 → 0.5.20

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 (149) hide show
  1. package/CHANGELOG.md +172 -79
  2. package/_packed_docs/CHANGELOG.md +172 -79
  3. package/_packed_docs/INDEX.md +244 -116
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +98 -69
  5. package/_packed_docs/agent.md/SCHEMA.md +245 -205
  6. package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +11 -11
  7. 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 +42 -15
  8. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +4 -4
  9. package/_packed_docs/protocol/01-/350/272/253/344/273/275/344/270/216/345/207/255/350/257/201/345/215/217/350/256/256-auth.md +45 -134
  10. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1072 -1072
  11. package/_packed_docs/protocol/07-/351/224/231/350/257/257/347/240/201/344/270/216/347/212/266/346/200/201/346/234/272.md +5 -20
  12. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +687 -668
  13. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +19 -28
  14. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +443 -0
  15. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.0.md +161 -0
  16. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +658 -0
  17. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
  18. package/_packed_docs/protocol/agent.md/SCHEMA.md +4 -0
  19. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +1 -1
  20. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +68 -61
  21. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +1 -1
  22. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +279 -285
  23. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +3 -3
  24. package/_packed_docs/sdk/09-group-rpc-manual.md +271 -235
  25. package/_packed_docs/sdk/09-message-rpc-manual.md +47 -6
  26. package/_packed_docs/sdk/09-storage-rpc-manual.md +70 -68
  27. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +63 -34
  28. package/_packed_docs/sdk/INDEX.md +105 -34
  29. package/_packed_docs/sdk/Ownership-SDK/345/256/236/347/216/260/346/214/207/345/215/227.md +169 -0
  30. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/227.md +349 -0
  31. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +922 -0
  32. package/_packed_docs/sdk/Ownership-SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +270 -0
  33. package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +300 -0
  34. package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +382 -0
  35. package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -0
  36. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +476 -0
  37. package/_packed_docs/sdk/Ownership/347/256/241/347/220/206/344/270/223/351/242/230.md +559 -0
  38. package/_packed_docs/sdk/ProtectedHeaders/347/224/250/346/263/225/344/270/223/351/242/230.md +299 -0
  39. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +173 -0
  40. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +211 -0
  41. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/345/256/236/347/216/260/347/247/273/346/244/215/346/214/207/345/215/227.md +331 -0
  42. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +207 -0
  43. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/344/273/243/347/240/201/345/256/241/346/237/245/346/212/245/345/221/212.md +252 -0
  44. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/345/256/236/346/226/275/350/256/241/345/210/222.md +156 -0
  45. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/346/226/271/346/241/210.md +273 -0
  46. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/350/257/246/347/273/206/350/256/276/350/256/241.md +211 -0
  47. package/dist/agent-md-schema.d.ts +17 -3
  48. package/dist/agent-md-schema.d.ts.map +1 -1
  49. package/dist/agent-md-schema.js +94 -26
  50. package/dist/agent-md-schema.js.map +1 -1
  51. package/dist/agent-md.d.ts +54 -3
  52. package/dist/agent-md.d.ts.map +1 -1
  53. package/dist/agent-md.js +711 -65
  54. package/dist/agent-md.js.map +1 -1
  55. package/dist/aid-store.d.ts +58 -46
  56. package/dist/aid-store.d.ts.map +1 -1
  57. package/dist/aid-store.js +254 -248
  58. package/dist/aid-store.js.map +1 -1
  59. package/dist/aid.d.ts +4 -1
  60. package/dist/aid.d.ts.map +1 -1
  61. package/dist/aid.js +27 -33
  62. package/dist/aid.js.map +1 -1
  63. package/dist/auth.d.ts +4 -1
  64. package/dist/auth.d.ts.map +1 -1
  65. package/dist/auth.js +426 -39
  66. package/dist/auth.js.map +1 -1
  67. package/dist/bundle.js +38179 -32054
  68. package/dist/cert-utils.d.ts +50 -2
  69. package/dist/cert-utils.d.ts.map +1 -1
  70. package/dist/cert-utils.js +105 -47
  71. package/dist/cert-utils.js.map +1 -1
  72. package/dist/client/delivery.d.ts +22 -1
  73. package/dist/client/delivery.d.ts.map +1 -1
  74. package/dist/client/delivery.js +389 -78
  75. package/dist/client/delivery.js.map +1 -1
  76. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  77. package/dist/client/rpc-pipeline.js +103 -77
  78. package/dist/client/rpc-pipeline.js.map +1 -1
  79. package/dist/client/v2-e2ee.d.ts +1 -0
  80. package/dist/client/v2-e2ee.d.ts.map +1 -1
  81. package/dist/client/v2-e2ee.js +182 -89
  82. package/dist/client/v2-e2ee.js.map +1 -1
  83. package/dist/client.d.ts +100 -1
  84. package/dist/client.d.ts.map +1 -1
  85. package/dist/client.js +1220 -25
  86. package/dist/client.js.map +1 -1
  87. package/dist/crypto.d.ts.map +1 -1
  88. package/dist/crypto.js +8 -6
  89. package/dist/crypto.js.map +1 -1
  90. package/dist/errors.d.ts.map +1 -1
  91. package/dist/errors.js +3 -2
  92. package/dist/errors.js.map +1 -1
  93. package/dist/events.d.ts +9 -2
  94. package/dist/events.d.ts.map +1 -1
  95. package/dist/events.js +72 -13
  96. package/dist/events.js.map +1 -1
  97. package/dist/facades.d.ts +82 -2
  98. package/dist/facades.d.ts.map +1 -1
  99. package/dist/facades.js +100 -6
  100. package/dist/facades.js.map +1 -1
  101. package/dist/index.d.ts +7 -3
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js +3 -2
  104. package/dist/index.js.map +1 -1
  105. package/dist/keystore/index.d.ts +17 -0
  106. package/dist/keystore/index.d.ts.map +1 -1
  107. package/dist/keystore/indexeddb-shared.d.ts +10 -1
  108. package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
  109. package/dist/keystore/indexeddb-shared.js +111 -0
  110. package/dist/keystore/indexeddb-shared.js.map +1 -1
  111. package/dist/keystore/indexeddb-token-store.d.ts +7 -1
  112. package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
  113. package/dist/keystore/indexeddb-token-store.js +30 -1
  114. package/dist/keystore/indexeddb-token-store.js.map +1 -1
  115. package/dist/org-protocol.d.ts +98 -0
  116. package/dist/org-protocol.d.ts.map +1 -0
  117. package/dist/org-protocol.js +1802 -0
  118. package/dist/org-protocol.js.map +1 -0
  119. package/dist/ownership-proof.d.ts +93 -5
  120. package/dist/ownership-proof.d.ts.map +1 -1
  121. package/dist/ownership-proof.js +239 -53
  122. package/dist/ownership-proof.js.map +1 -1
  123. package/dist/ownership-v21.d.ts +194 -0
  124. package/dist/ownership-v21.d.ts.map +1 -0
  125. package/dist/ownership-v21.js +678 -0
  126. package/dist/ownership-v21.js.map +1 -0
  127. package/dist/ownership-workflow.d.ts +151 -0
  128. package/dist/ownership-workflow.d.ts.map +1 -0
  129. package/dist/ownership-workflow.js +1229 -0
  130. package/dist/ownership-workflow.js.map +1 -0
  131. package/dist/seq-tracker.d.ts +1 -0
  132. package/dist/seq-tracker.d.ts.map +1 -1
  133. package/dist/seq-tracker.js +10 -2
  134. package/dist/seq-tracker.js.map +1 -1
  135. package/dist/tools/cross-sdk-agent.d.ts +83 -0
  136. package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
  137. package/dist/tools/cross-sdk-agent.js +415 -7
  138. package/dist/tools/cross-sdk-agent.js.map +1 -1
  139. package/dist/transport.d.ts +7 -0
  140. package/dist/transport.d.ts.map +1 -1
  141. package/dist/transport.js +90 -2
  142. package/dist/transport.js.map +1 -1
  143. package/dist/v2/e2ee/decrypt.d.ts +1 -0
  144. package/dist/v2/e2ee/decrypt.d.ts.map +1 -1
  145. package/dist/v2/e2ee/decrypt.js +9 -1
  146. package/dist/v2/e2ee/decrypt.js.map +1 -1
  147. package/dist/version.d.ts +1 -1
  148. package/dist/version.js +1 -1
  149. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { slotIsolationKey } from '../config.js';
2
2
  import { normalizeGroupAid } from '../group-id.js';
3
+ import { SeqTracker } from '../seq-tracker.js';
3
4
  import { ConnectionState, isJsonObject } from '../types.js';
4
5
  import { hasGroupMentionModeField, normalizeGroupMentionMode } from './mention-mode.js';
5
6
  const PUSHED_SEQS_LIMIT = 50_000;
@@ -132,10 +133,14 @@ export class MessageDeliveryEngine {
132
133
  pendingPullNoProgressAcks = null;
133
134
  onlineUnreadHintTargets = null;
134
135
  onlineUnreadHintOwners = null;
136
+ backgroundPullDemands = new Map();
137
+ backgroundPullTimer = null;
138
+ backgroundPullRecoveryQueued = false;
135
139
  realtimeAcking = null;
136
140
  pendingP2PInlineAcks = null;
137
141
  pendingGroupInlineAcks = null;
138
142
  pendingGroupInlineAckNamespaces = null;
143
+ pendingTailWindows = new Map();
139
144
  volatileContext = '';
140
145
  volatilePullCursorValue = '';
141
146
  volatileSeenMessages = new Map();
@@ -159,11 +164,17 @@ export class MessageDeliveryEngine {
159
164
  this.pendingPullNoProgressAcks = null;
160
165
  this.onlineUnreadHintTargets = null;
161
166
  this.onlineUnreadHintOwners = null;
167
+ this.backgroundPullDemands.clear();
168
+ if (this.backgroundPullTimer !== null)
169
+ clearTimeout(this.backgroundPullTimer);
170
+ this.backgroundPullTimer = null;
171
+ this.backgroundPullRecoveryQueued = false;
162
172
  this.realtimeTailResults = null;
163
173
  this.realtimeAcking = null;
164
174
  this.pendingP2PInlineAcks = null;
165
175
  this.pendingGroupInlineAcks = null;
166
176
  this.pendingGroupInlineAckNamespaces = null;
177
+ this.pendingTailWindows.clear();
167
178
  this.syncingNamespaces.clear();
168
179
  this.syncSessions.clear();
169
180
  }
@@ -186,6 +197,35 @@ export class MessageDeliveryEngine {
186
197
  captureInlineGeneration() {
187
198
  return this.inlineGeneration;
188
199
  }
200
+ deferTailWindow(ns, proof) {
201
+ const tracker = new SeqTracker();
202
+ tracker.restoreNamespaceSnapshot(ns, this.runtime.client._seqTracker.snapshotNamespace(ns));
203
+ tracker.commitTailWindow(ns, proof);
204
+ this.pendingTailWindows.set(ns, proof);
205
+ }
206
+ prospectiveTailState(ns) {
207
+ const proof = this.pendingTailWindows.get(ns);
208
+ if (!proof)
209
+ return this.syncState(ns);
210
+ const tracker = new SeqTracker();
211
+ tracker.restoreNamespaceSnapshot(ns, this.runtime.client._seqTracker.snapshotNamespace(ns));
212
+ tracker.commitTailWindow(ns, proof);
213
+ return tracker.getSyncState(ns);
214
+ }
215
+ pendingTailDeliveryThrough(ns, scanThrough) {
216
+ const proof = this.pendingTailWindows.get(ns);
217
+ return proof && scanThrough >= proof.windowStartSeq - 1 ? Math.max(scanThrough, proof.headSeq) : scanThrough;
218
+ }
219
+ commitPendingTailWindow(ns, scanThrough) {
220
+ const proof = this.pendingTailWindows.get(ns);
221
+ if (proof && scanThrough >= proof.windowStartSeq - 1)
222
+ this.runtime.client._seqTracker.commitTailWindow(ns, proof);
223
+ }
224
+ completePendingTailWindow(ns) {
225
+ const proof = this.pendingTailWindows.get(ns);
226
+ if (proof && this.runtime.client._seqTracker.getContiguousSeq(ns) >= proof.headSeq)
227
+ this.pendingTailWindows.delete(ns);
228
+ }
189
229
  beginRealtimeSync(ns) {
190
230
  if (!ns || this.realtimeSyncing?.has(ns))
191
231
  return false;
@@ -294,8 +334,120 @@ export class MessageDeliveryEngine {
294
334
  if (!ns)
295
335
  return;
296
336
  this.schedulePendingPullIfNeeded(ns, 'pull-gate-idle');
337
+ this.requestBackgroundPullRecovery();
338
+ }
339
+ recordBackgroundPullDemand(ns, throughSeq = 0, probe = false) {
340
+ const demand = this.backgroundPullDemands.get(ns) ?? {
341
+ throughSeq: 0, hasMore: false, probe: false, afterSeq: -1, retryAt: 0,
342
+ };
343
+ demand.throughSeq = Math.max(demand.throughSeq, positiveSafeSequenceHint(throughSeq));
344
+ demand.probe ||= probe;
345
+ this.backgroundPullDemands.set(ns, demand);
346
+ return demand;
347
+ }
348
+ observeBackgroundPullResponse(ns, afterSeq, result, limit = 50) {
349
+ if (!ns || this.runtime.client._sessionOptions?.background_sync === false)
350
+ return;
351
+ const cursor = isJsonObject(result.cursor) ? result.cursor : null;
352
+ const rows = Array.isArray(result.messages) ? result.messages : Array.isArray(result.events) ? result.events : [];
353
+ let throughSeq = Math.max(positiveSafeSequenceHint(cursor?.latest_seq), positiveSafeSequenceHint(result.latest_seq), positiveSafeSequenceHint(result.latest_message_seq), positiveSafeSequenceHint(result.latest_event_seq));
354
+ for (const row of rows) {
355
+ if (!isJsonObject(row) || this.isVolatileMessage(row))
356
+ continue;
357
+ throughSeq = Math.max(throughSeq, positiveSafeSequenceHint(row.seq ?? row.event_seq));
358
+ }
359
+ const demand = this.recordBackgroundPullDemand(ns, throughSeq);
360
+ if (afterSeq >= demand.afterSeq) {
361
+ demand.afterSeq = afterSeq;
362
+ demand.hasMore = result.has_more === true
363
+ || positiveSafeSequenceHint(result.remaining) > 0
364
+ || (!Object.prototype.hasOwnProperty.call(result, 'has_more') && limit > 0 && rows.length >= limit);
365
+ demand.probe = false;
366
+ }
367
+ this.requestBackgroundPullRecovery();
368
+ }
369
+ finishBackgroundPull(ns, afterSeq, failed) {
370
+ const demand = this.backgroundPullDemands.get(ns);
371
+ if (demand) {
372
+ const state = this.syncState(ns);
373
+ const ack = state.ack;
374
+ if (ack < state.tail - 1)
375
+ demand.throughSeq = Math.max(demand.throughSeq, state.tail - 1);
376
+ demand.probe = failed;
377
+ if (!demand.probe && !demand.hasMore && ack >= demand.throughSeq)
378
+ this.backgroundPullDemands.delete(ns);
379
+ else
380
+ demand.retryAt = failed || ack <= afterSeq || ack <= (this.pendingPullNoProgressAcks?.get(ns) ?? -1)
381
+ ? Date.now() + P2P_GAP_FILL_RETRY_BASE_MS
382
+ : 0;
383
+ }
384
+ this.requestBackgroundPullRecovery();
385
+ }
386
+ requestBackgroundPullRecovery() {
387
+ if (this.backgroundPullRecoveryQueued)
388
+ return;
389
+ const generation = this.captureInlineGeneration();
390
+ this.backgroundPullRecoveryQueued = true;
391
+ void Promise.resolve().then(() => {
392
+ if (!this.isInlineGenerationCurrent(generation))
393
+ return;
394
+ this.backgroundPullRecoveryQueued = false;
395
+ const client = this.runtime.client;
396
+ if (!client._aid || client._sessionOptions?.background_sync === false
397
+ || client.state !== ConnectionState.READY || client._closing)
398
+ return;
399
+ if (this.backgroundPullTimer !== null)
400
+ clearTimeout(this.backgroundPullTimer);
401
+ this.backgroundPullTimer = null;
402
+ let retryAt = 0;
403
+ for (const [ns, demand] of this.backgroundPullDemands) {
404
+ if (ns.startsWith('p2p:') && ns !== `p2p:${client._aid}`)
405
+ continue;
406
+ const dedupKey = ns.startsWith('p2p:') ? `p2p_pull:${ns}`
407
+ : ns.startsWith('group_event:') ? `group_event_pull:${ns}` : `group_pull:${ns}`;
408
+ if (client._gapFillDone.has(dedupKey) || this.onlineUnreadHintOwners?.has(ns)
409
+ || this.realtimeSyncing?.has(ns) || client._rpcPipeline?.hasPullActivity?.(ns, false))
410
+ continue;
411
+ const ack = this.syncState(ns).ack;
412
+ if (!demand.probe && !demand.hasMore && ack >= demand.throughSeq) {
413
+ this.backgroundPullDemands.delete(ns);
414
+ continue;
415
+ }
416
+ if (demand.retryAt > Date.now()) {
417
+ retryAt = retryAt === 0 ? demand.retryAt : Math.min(retryAt, demand.retryAt);
418
+ continue;
419
+ }
420
+ if (ns.startsWith('p2p:'))
421
+ client._safeAsync(this.fillP2pGap());
422
+ else if (ns.startsWith('group_event:'))
423
+ client._safeAsync(this.fillGroupEventGap(ns.slice('group_event:'.length)));
424
+ else if (ns.startsWith('group:'))
425
+ client._safeAsync(this.fillGroupGap(ns.slice('group:'.length)));
426
+ }
427
+ if (retryAt > 0) {
428
+ this.backgroundPullTimer = globalThis.setTimeout(() => {
429
+ this.backgroundPullTimer = null;
430
+ if (this.isInlineGenerationCurrent(generation))
431
+ this.requestBackgroundPullRecovery();
432
+ }, Math.max(0, retryAt - Date.now()));
433
+ }
434
+ });
435
+ }
436
+ onForegroundPullsDrained() {
437
+ this.requestBackgroundPullRecovery();
438
+ const client = this.runtime.client;
439
+ if (!client._aid || client._sessionOptions?.background_sync === false
440
+ || client.state !== ConnectionState.READY || client._closing)
441
+ return;
442
+ const ns = `p2p:${client._aid}`;
443
+ const state = this.syncState(ns);
444
+ const maxSeen = client._seqTracker.getMaxSeenSeq(ns);
445
+ if (maxSeen <= state.ack && state.ack >= state.tail - 1)
446
+ return;
447
+ client._safeAsync(this.fillP2pGap());
297
448
  }
298
449
  onPullWorkSettled() {
450
+ this.requestBackgroundPullRecovery();
299
451
  const pipeline = this.runtime.client._rpcPipeline;
300
452
  if (pipeline?.hasAnyPullActivity?.() === true)
301
453
  return;
@@ -952,9 +1104,7 @@ export class MessageDeliveryEngine {
952
1104
  const pageInlineGeneration = this.captureInlineGeneration();
953
1105
  const deferredServerCursor = Number(coordinator.pendingForwardCursor(ns, pageContigBefore) ?? 0);
954
1106
  const deferredForwardAck = Number(coordinator.pendingForwardAck(ns, pageContigBefore) ?? 0);
955
- const pageTrackerSnapshot = typeof client._seqTracker.snapshotNamespace === 'function'
956
- ? client._seqTracker.snapshotNamespace(ns)
957
- : null;
1107
+ let pageTrackerSnapshot;
958
1108
  response._contig_before = pageContigBefore;
959
1109
  const cursor = isJsonObject(response.cursor)
960
1110
  ? response.cursor
@@ -1005,6 +1155,15 @@ export class MessageDeliveryEngine {
1005
1155
  const contiguousSeq = Number.isSafeInteger(result.contiguous_seq)
1006
1156
  ? result.contiguous_seq
1007
1157
  : undefined;
1158
+ const scanThrough = Math.max(pageContigBefore, contiguousSeq ?? 0, retentionFloor, visibilityFloor, deferredServerCursor, ...messages.map((message) => positiveSafeSequenceHint(message.seq)));
1159
+ const state = this.syncState(ns);
1160
+ const deliveryThrough = this.pendingTailDeliveryThrough(ns, scanThrough >= state.tail - 1 ? Math.max(scanThrough, state.head) : scanThrough);
1161
+ await this.drainOrderedMessages(ns, undefined, false, false, undefined, 'pull', deliveryThrough);
1162
+ this.ensurePullOperationCurrent(ns);
1163
+ if (!this.isInlineGenerationCurrent(pageInlineGeneration))
1164
+ throw new Error('pull invalidated');
1165
+ pageTrackerSnapshot = client._seqTracker.snapshotNamespace(ns);
1166
+ this.commitPendingTailWindow(ns, scanThrough);
1008
1167
  if (contiguousSeq === undefined)
1009
1168
  client._seqTracker.onPullResult(ns, messages, afterSeq);
1010
1169
  else
@@ -1014,16 +1173,19 @@ export class MessageDeliveryEngine {
1014
1173
  client._seqTracker.forceContiguousSeq(ns, commitTarget);
1015
1174
  }
1016
1175
  if (client._seqTracker.getContiguousSeq(ns) !== pageContigBefore) {
1017
- this.ensurePullOperationCurrent(ns);
1018
- await this.drainOrderedMessages(ns, undefined, false, false);
1019
1176
  this.ensurePullOperationCurrent(ns);
1020
1177
  await client._commitSeqTrackerState(ns);
1178
+ this.ensurePullOperationCurrent(ns);
1179
+ if (!this.isInlineGenerationCurrent(pageInlineGeneration))
1180
+ throw new Error('pull invalidated');
1021
1181
  }
1182
+ this.completePendingTailWindow(ns);
1022
1183
  committed = true;
1023
1184
  }
1024
1185
  catch (exc) {
1025
- if (this.isPullOperationCurrent(ns) && !committed && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
1026
- client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
1186
+ if (this.isPullOperationCurrent(ns) && this.isInlineGenerationCurrent(pageInlineGeneration)
1187
+ && pageTrackerSnapshot !== undefined && !committed && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
1188
+ client._seqTracker._restoreNamespaceSnapshotPreservingPush(ns, pageTrackerSnapshot);
1027
1189
  this.dropSeqTrackerPending(ns);
1028
1190
  }
1029
1191
  throw exc;
@@ -1103,6 +1265,8 @@ export class MessageDeliveryEngine {
1103
1265
  }
1104
1266
  if (pullResponse) {
1105
1267
  await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, source, ns, batch));
1268
+ if (pullScoped)
1269
+ this.ensurePullOperationCurrent(ns);
1106
1270
  return;
1107
1271
  }
1108
1272
  await client._publishAppEvent(event, payload, source, ns, batch);
@@ -1111,6 +1275,10 @@ export class MessageDeliveryEngine {
1111
1275
  }
1112
1276
  async publishOrderedGroupChanged(payload, source = 'ordered') {
1113
1277
  const client = this.runtime.client;
1278
+ const generation = this.captureInlineGeneration();
1279
+ await client._publishAppEvent('group.changed', payload, source);
1280
+ if (!this.isInlineGenerationCurrent(generation))
1281
+ throw new Error('pull invalidated');
1114
1282
  if (isJsonObject(payload)) {
1115
1283
  const eventPayload = payload;
1116
1284
  client._groupState?.handleGroupChangedV2Membership?.(eventPayload);
@@ -1121,7 +1289,6 @@ export class MessageDeliveryEngine {
1121
1289
  client._cleanupDissolvedGroup?.(groupId);
1122
1290
  }
1123
1291
  }
1124
- await client._publishAppEvent('group.changed', payload, source);
1125
1292
  }
1126
1293
  isInstanceScopedMessageEvent(event) {
1127
1294
  return event === 'message.received'
@@ -1559,6 +1726,7 @@ export class MessageDeliveryEngine {
1559
1726
  }
1560
1727
  async publishMessageRecallTombstone(seq, message, source = 'push') {
1561
1728
  const client = this.runtime.client;
1729
+ const generation = this.captureInlineGeneration();
1562
1730
  const eventPayload = this.recallEventFromMessage(message);
1563
1731
  if (!eventPayload)
1564
1732
  return false;
@@ -1572,13 +1740,15 @@ export class MessageDeliveryEngine {
1572
1740
  client._clientLog.debug(`message.recalled dedup suppressed: seq=${String(seq)} key=${dedupKey}`);
1573
1741
  return false;
1574
1742
  }
1743
+ await client._publishAppEvent('message.recalled', eventPayload, source);
1744
+ if (!this.isInlineGenerationCurrent(generation))
1745
+ throw new Error('pull invalidated');
1575
1746
  seen.set(dedupKey, Date.now());
1576
1747
  if (seen.size > MESSAGE_RECALL_SEEN_LIMIT) {
1577
1748
  const drop = [...seen.entries()].sort((a, b) => a[1] - b[1]).slice(0, seen.size - MESSAGE_RECALL_SEEN_LIMIT);
1578
1749
  for (const [oldKey] of drop)
1579
1750
  seen.delete(oldKey);
1580
1751
  }
1581
- await client._publishAppEvent('message.recalled', eventPayload, source);
1582
1752
  client._clientLog.debug(`message.recalled published: seq=${String(seq)} ids=${JSON.stringify(eventPayload.message_ids)}`);
1583
1753
  return true;
1584
1754
  }
@@ -1663,6 +1833,7 @@ export class MessageDeliveryEngine {
1663
1833
  }
1664
1834
  async publishGroupRecallTombstone(groupId, seq, message, source = 'push') {
1665
1835
  const client = this.runtime.client;
1836
+ const generation = this.captureInlineGeneration();
1666
1837
  const eventPayload = this.recallEventFromGroupMessage(message);
1667
1838
  if (!eventPayload)
1668
1839
  return false;
@@ -1678,13 +1849,15 @@ export class MessageDeliveryEngine {
1678
1849
  client._clientLog.debug(`group.message_recalled dedup suppressed: group=${groupId} seq=${String(seq)} key=${dedupKey}`);
1679
1850
  return false;
1680
1851
  }
1852
+ await client._publishAppEvent('group.message_recalled', eventPayload, source);
1853
+ if (!this.isInlineGenerationCurrent(generation))
1854
+ throw new Error('pull invalidated');
1681
1855
  seen.set(dedupKey, Date.now());
1682
1856
  if (seen.size > GROUP_RECALL_SEEN_LIMIT) {
1683
1857
  const drop = [...seen.entries()].sort((a, b) => a[1] - b[1]).slice(0, seen.size - GROUP_RECALL_SEEN_LIMIT);
1684
1858
  for (const [oldKey] of drop)
1685
1859
  seen.delete(oldKey);
1686
1860
  }
1687
- await client._publishAppEvent('group.message_recalled', eventPayload, source);
1688
1861
  client._clientLog.debug(`group.message_recalled published: group=${groupId} seq=${String(seq)} ids=${JSON.stringify(eventPayload.message_ids)}`);
1689
1862
  return true;
1690
1863
  }
@@ -1755,7 +1928,13 @@ export class MessageDeliveryEngine {
1755
1928
  && canonicalSource !== 'direct') {
1756
1929
  normalized = { ...normalized, source: canonicalSource };
1757
1930
  }
1758
- client._dispatcher.enqueue(event, normalized);
1931
+ if (['message.received', 'group.message_created', 'message.recalled', 'group.message_recalled', 'group.changed'].includes(event)
1932
+ && typeof client._dispatcher._publishAndWait === 'function') {
1933
+ await client._dispatcher._publishAndWait(event, normalized);
1934
+ }
1935
+ else {
1936
+ client._dispatcher.enqueue(event, normalized);
1937
+ }
1759
1938
  if (generation !== this.deliveryGeneration)
1760
1939
  return;
1761
1940
  if (canonicalSource !== 'pull' && canonicalSource !== 'pending_retry'
@@ -1997,6 +2176,8 @@ export class MessageDeliveryEngine {
1997
2176
  }
1998
2177
  }
1999
2178
  catch (exc) {
2179
+ if (exc?.stringCode === 'persistence_unavailable')
2180
+ throw exc;
2000
2181
  client._clientLog.warn(`P2P push processing failed:${String(exc)}`);
2001
2182
  if (isJsonObject(data)) {
2002
2183
  const src = data;
@@ -2155,7 +2336,14 @@ export class MessageDeliveryEngine {
2155
2336
  return;
2156
2337
  }
2157
2338
  }
2339
+ const generation = this.captureInlineGeneration();
2340
+ const dedupKey = `group_pull:${ns}`;
2341
+ this.recordBackgroundPullDemand(ns, positiveSafeSequenceHint(notification.seq), true);
2342
+ if (client._gapFillDone.has(dedupKey))
2343
+ return;
2344
+ client._gapFillDone.add(dedupKey);
2158
2345
  let rawCompleted = false;
2346
+ let failed = false;
2159
2347
  try {
2160
2348
  const key = client._rpcPipeline.pullGateKeyForCall('group.pull', {
2161
2349
  group_id: groupId,
@@ -2171,12 +2359,17 @@ export class MessageDeliveryEngine {
2171
2359
  limit: 50,
2172
2360
  _rpc_background: true,
2173
2361
  }, { background: true });
2362
+ if (!this.isInlineGenerationCurrent(generation))
2363
+ throw new Error('pull invalidated');
2174
2364
  rawCompleted = true;
2365
+ if (isJsonObject(result))
2366
+ this.observeBackgroundPullResponse(ns, afterSeq, result, 50);
2175
2367
  await this.commitPlainForwardPage(ns, afterSeq, 'group.pull', result, groupId);
2176
2368
  this.prunePushedSeqs(ns);
2177
2369
  }, true);
2178
2370
  }
2179
2371
  catch (exc) {
2372
+ failed = true;
2180
2373
  // 页发布或本地提交失败不能 ACK;提交后 ACK 失败则保留 pending。
2181
2374
  // 两种情况都不能再把原始通知当作第二条业务消息发布。
2182
2375
  client._clientLog.warn(`auto pull group message commit/ack failed:${String(exc)}`);
@@ -2184,6 +2377,12 @@ export class MessageDeliveryEngine {
2184
2377
  await client._publishAppEvent('group.message_created', notification, 'push');
2185
2378
  }
2186
2379
  }
2380
+ finally {
2381
+ if (this.isInlineGenerationCurrent(generation)) {
2382
+ client._gapFillDone.delete(dedupKey);
2383
+ this.finishBackgroundPull(ns, afterSeq, failed);
2384
+ }
2385
+ }
2187
2386
  }
2188
2387
  scheduleP2pGapRetry(ns, retryAttempt) {
2189
2388
  const client = this.runtime.client;
@@ -2207,6 +2406,7 @@ export class MessageDeliveryEngine {
2207
2406
  }
2208
2407
  async fillP2pGap(retryAttempt = 0) {
2209
2408
  const client = this.runtime.client;
2409
+ const generation = this.captureInlineGeneration();
2210
2410
  if (client._sessionOptions?.background_sync === false)
2211
2411
  return;
2212
2412
  if (client.state !== ConnectionState.READY || client._closing)
@@ -2218,9 +2418,11 @@ export class MessageDeliveryEngine {
2218
2418
  const dedupKey = `p2p_pull:${ns}`;
2219
2419
  if (client._gapFillDone.has(dedupKey))
2220
2420
  return;
2421
+ this.recordBackgroundPullDemand(ns, 0, true);
2221
2422
  client._gapFillDone.add(dedupKey);
2222
2423
  this.runtime.delivery.setGapFillActive(true);
2223
2424
  let filled = 0;
2425
+ let failed = false;
2224
2426
  try {
2225
2427
  const key = client._rpcPipeline.pullGateKeyForCall('message.pull', { after_seq: afterSeq, limit: 50 });
2226
2428
  const messages = await client._rpcPipeline.runPullSerialized(key, () => client._pullV2(afterSeq, 50, { gateLocked: true }), true);
@@ -2236,22 +2438,27 @@ export class MessageDeliveryEngine {
2236
2438
  client._clientLog.warn(`P2P message gap-fill V2 unavailable, used plain pull fallback:${String(formatDeliveryError(exc))}`);
2237
2439
  }
2238
2440
  catch (fallbackExc) {
2441
+ failed = true;
2239
2442
  client._clientLog.warn(`P2P message gap-fill plain fallback failed:${String(formatDeliveryError(fallbackExc))}`);
2240
- this.scheduleP2pGapRetry(ns, retryAttempt + 1);
2241
2443
  }
2242
2444
  }
2243
2445
  else {
2446
+ failed = true;
2244
2447
  client._clientLog.warn(`P2P message gap-fill failed:${String(formatDeliveryError(exc))}`);
2245
2448
  }
2246
2449
  }
2247
2450
  finally {
2248
- client._gapFillDone.delete(dedupKey);
2249
- this.runtime.delivery.setGapFillActive(false);
2250
- this.schedulePendingPullIfNeeded(ns, 'gap-fill-complete');
2451
+ if (this.isInlineGenerationCurrent(generation)) {
2452
+ client._gapFillDone.delete(dedupKey);
2453
+ this.runtime.delivery.setGapFillActive(false);
2454
+ this.schedulePendingPullIfNeeded(ns, 'gap-fill-complete');
2455
+ this.finishBackgroundPull(ns, afterSeq, failed);
2456
+ }
2251
2457
  }
2252
2458
  }
2253
2459
  async fillP2pGapWithPlainPull(ns, afterSeq) {
2254
2460
  const client = this.runtime.client;
2461
+ const generation = this.captureInlineGeneration();
2255
2462
  const params = {
2256
2463
  after_seq: afterSeq,
2257
2464
  limit: 50,
@@ -2260,11 +2467,16 @@ export class MessageDeliveryEngine {
2260
2467
  _rpc_background: true,
2261
2468
  };
2262
2469
  const raw = await client._rpcPipeline.rawCall('message.pull', params, { background: true });
2470
+ if (!this.isInlineGenerationCurrent(generation))
2471
+ throw new Error('pull invalidated');
2472
+ if (isJsonObject(raw))
2473
+ this.observeBackgroundPullResponse(ns, afterSeq, raw, 50);
2263
2474
  const page = await this.commitPlainForwardPage(ns, afterSeq, 'message.pull', raw);
2264
2475
  return page.publishedCount;
2265
2476
  }
2266
2477
  async fillGroupGap(groupId) {
2267
2478
  const client = this.runtime.client;
2479
+ const generation = this.captureInlineGeneration();
2268
2480
  if (client._sessionOptions?.background_sync === false)
2269
2481
  return;
2270
2482
  if (client.state !== ConnectionState.READY || client._closing)
@@ -2277,29 +2489,53 @@ export class MessageDeliveryEngine {
2277
2489
  const dedupKey = `group_pull:${ns}`;
2278
2490
  if (client._gapFillDone.has(dedupKey))
2279
2491
  return;
2492
+ this.recordBackgroundPullDemand(ns, 0, true);
2280
2493
  client._gapFillDone.add(dedupKey);
2281
2494
  this.runtime.delivery.setGapFillActive(true);
2282
2495
  let filled = 0;
2496
+ let failed = false;
2283
2497
  try {
2284
2498
  const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 });
2285
- const messages = await client._rpcPipeline.runPullSerialized(key, () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true }), true);
2499
+ const messages = await client._rpcPipeline.runPullSerialized(key, async () => {
2500
+ if (client._v2Session)
2501
+ return await client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true });
2502
+ const result = await client._rpcPipeline.rawCall('group.pull', {
2503
+ group_id: groupId,
2504
+ after_message_seq: afterSeq,
2505
+ device_id: client._deviceId,
2506
+ slot_id: client._slotId,
2507
+ limit: 50,
2508
+ _rpc_background: true,
2509
+ }, { background: true });
2510
+ if (!this.isInlineGenerationCurrent(generation))
2511
+ throw new Error('pull invalidated');
2512
+ if (isJsonObject(result))
2513
+ this.observeBackgroundPullResponse(ns, afterSeq, result, 50);
2514
+ await this.commitPlainForwardPage(ns, afterSeq, 'group.pull', result, groupId);
2515
+ return isJsonObject(result) && Array.isArray(result.messages) ? result.messages : [];
2516
+ }, true);
2286
2517
  filled = messages.length;
2287
2518
  this.prunePushedSeqs(ns);
2288
2519
  }
2289
2520
  catch (exc) {
2521
+ failed = true;
2290
2522
  client._clientLog.warn(`group message gap-fill failed:${String(exc)}`);
2291
2523
  }
2292
2524
  finally {
2293
- client._gapFillDone.delete(dedupKey);
2294
- this.runtime.delivery.setGapFillActive(false);
2295
- if (filled > 0 && client._seqTracker.getContiguousSeq(ns) > afterSeq) {
2296
- client._safeAsync(this.fillGroupGap(groupId));
2525
+ if (this.isInlineGenerationCurrent(generation)) {
2526
+ client._gapFillDone.delete(dedupKey);
2527
+ this.runtime.delivery.setGapFillActive(false);
2528
+ if (filled > 0 && client._seqTracker.getContiguousSeq(ns) > afterSeq) {
2529
+ this.recordBackgroundPullDemand(ns, 0, true);
2530
+ }
2531
+ this.schedulePendingPullIfNeeded(ns, 'gap-fill-complete');
2532
+ this.finishBackgroundPull(ns, afterSeq, failed);
2297
2533
  }
2298
- this.schedulePendingPullIfNeeded(ns, 'gap-fill-complete');
2299
2534
  }
2300
2535
  }
2301
2536
  async fillGroupEventGap(groupId, options) {
2302
2537
  const client = this.runtime.client;
2538
+ const generation = this.captureInlineGeneration();
2303
2539
  if (client._sessionOptions?.background_sync === false)
2304
2540
  return;
2305
2541
  if (client.state !== ConnectionState.READY || client._closing)
@@ -2316,6 +2552,7 @@ export class MessageDeliveryEngine {
2316
2552
  const dedupKey = `group_event_pull:${ns}`;
2317
2553
  if (client._gapFillDone.has(dedupKey))
2318
2554
  return;
2555
+ this.recordBackgroundPullDemand(ns, 0, true);
2319
2556
  client._gapFillDone.add(dedupKey);
2320
2557
  this.runtime.delivery.setGapFillActive(true);
2321
2558
  let continuationAfterSeq = 0;
@@ -2335,6 +2572,10 @@ export class MessageDeliveryEngine {
2335
2572
  _rpc_background: true,
2336
2573
  };
2337
2574
  const result = await client.call('group.pull_events', request);
2575
+ if (!this.isInlineGenerationCurrent(generation))
2576
+ throw new Error('pull invalidated');
2577
+ if (isJsonObject(result))
2578
+ this.observeBackgroundPullResponse(ns, nextAfterSeq, result, 50);
2338
2579
  if (!isJsonObject(result))
2339
2580
  return;
2340
2581
  const events = result.events;
@@ -2343,23 +2584,12 @@ export class MessageDeliveryEngine {
2343
2584
  const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
2344
2585
  const eventObjects = events.filter((evt) => isJsonObject(evt));
2345
2586
  const contiguousSeq = Number.isSafeInteger(result.contiguous_seq) ? result.contiguous_seq : undefined;
2346
- if (contiguousSeq === undefined)
2347
- client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq);
2348
- else
2349
- client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq, contiguousSeq);
2350
2587
  const cursor = isJsonObject(result.cursor) ? result.cursor : null;
2351
2588
  const retentionFloor = Math.max(0, Number(result.retention_floor_event_seq ?? 0), Number(cursor?.retention_floor_seq ?? 0));
2352
2589
  const serverAckFloor = Math.max(0, Number(cursor?.current_seq ?? 0));
2353
2590
  const earliestAvailable = Number(result.earliest_available_event_seq ?? 0);
2354
2591
  const visibilityFloor = Math.max(0, Number(cursor?.join_seq ?? 0), Number.isFinite(earliestAvailable) && earliestAvailable > 0 ? earliestAvailable - 1 : 0);
2355
2592
  const effectiveFloor = Math.max(...[retentionFloor, serverAckFloor, visibilityFloor].filter((value) => Number.isFinite(value)));
2356
- if (effectiveFloor > 0) {
2357
- const contigBeforeFloor = client._seqTracker.getContiguousSeq(ns);
2358
- if (contigBeforeFloor < effectiveFloor) {
2359
- client._clientLog.info('group.pull_events floor advance: ns=' + ns + ' contiguous=' + contigBeforeFloor + ' -> effective_floor=' + effectiveFloor + ' retention_floor=' + retentionFloor + ' server_ack=' + serverAckFloor + ' visibility_floor=' + visibilityFloor);
2360
- client._seqTracker.forceContiguousSeq(ns, effectiveFloor);
2361
- }
2362
- }
2363
2593
  const eventSeqs = [];
2364
2594
  let hasDissolvedEvent = false;
2365
2595
  let publishedEventCount = 0;
@@ -2388,8 +2618,22 @@ export class MessageDeliveryEngine {
2388
2618
  publishedEventCount += 1;
2389
2619
  }
2390
2620
  }
2621
+ const deliveryThrough = Math.max(pageContigBefore, contiguousSeq ?? 0, effectiveFloor, ...eventSeqs);
2622
+ await this.drainOrderedMessages(ns, undefined, false, false, undefined, 'pull', deliveryThrough);
2623
+ if (!this.isInlineGenerationCurrent(generation))
2624
+ throw new Error('pull invalidated');
2625
+ if (hasDissolvedEvent) {
2626
+ this.backgroundPullDemands.delete(ns);
2627
+ break;
2628
+ }
2629
+ if (contiguousSeq === undefined)
2630
+ client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq);
2631
+ else
2632
+ client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq, contiguousSeq);
2633
+ if (effectiveFloor > client._seqTracker.getContiguousSeq(ns)) {
2634
+ client._seqTracker.forceContiguousSeq(ns, effectiveFloor);
2635
+ }
2391
2636
  const ackContig = client._seqTracker.getContiguousSeq(ns);
2392
- await this.drainOrderedMessages(ns);
2393
2637
  if (ackContig !== pageContigBefore && !hasDissolvedEvent) {
2394
2638
  this.persistSeq(ns);
2395
2639
  }
@@ -2429,15 +2673,12 @@ export class MessageDeliveryEngine {
2429
2673
  client._clientLog.warn(`group event gap-fill failed:${String(exc)}`);
2430
2674
  }
2431
2675
  finally {
2432
- client._gapFillDone.delete(dedupKey);
2433
- this.runtime.delivery.setGapFillActive(false);
2434
- if (!client._rpcPipeline)
2435
- this.syncStopped(failed ? 'aborted' : 'pull_drained');
2436
- if (singlePage && continuationAfterSeq > afterSeq) {
2437
- this.enqueueOnlineUnreadEventHint({
2438
- group_id: groupId,
2439
- _online_hint_after_event_seq: continuationAfterSeq,
2440
- });
2676
+ if (this.isInlineGenerationCurrent(generation)) {
2677
+ client._gapFillDone.delete(dedupKey);
2678
+ this.runtime.delivery.setGapFillActive(false);
2679
+ if (!client._rpcPipeline)
2680
+ this.syncStopped(failed ? 'aborted' : 'pull_drained');
2681
+ this.finishBackgroundPull(ns, afterSeq, failed);
2441
2682
  }
2442
2683
  }
2443
2684
  }
@@ -2493,6 +2734,9 @@ export class MessageDeliveryEngine {
2493
2734
  if (!groupId)
2494
2735
  return;
2495
2736
  const ns = `group:${groupId}`;
2737
+ const generation = this.captureInlineGeneration();
2738
+ const afterSeq = this.syncState(ns).ack;
2739
+ this.recordBackgroundPullDemand(ns, positiveSafeSequenceHint(data.seq), true);
2496
2740
  const targets = this.onlineUnreadHintTargets ?? new Map();
2497
2741
  const owners = this.onlineUnreadHintOwners ?? new Set();
2498
2742
  this.onlineUnreadHintTargets = targets;
@@ -2502,11 +2746,16 @@ export class MessageDeliveryEngine {
2502
2746
  return;
2503
2747
  owners.add(ns);
2504
2748
  client._safeAsync((async () => {
2749
+ let failed = false;
2750
+ let attemptAfterSeq = afterSeq;
2505
2751
  try {
2506
2752
  while (true) {
2507
2753
  const throughSeq = targets.get(ns) ?? 0;
2508
2754
  const ackBefore = this.syncState(ns).ack;
2755
+ attemptAfterSeq = ackBefore;
2509
2756
  await this.runGroupForwardRecovery(groupId, ns, 50, true, throughSeq);
2757
+ if (!this.isInlineGenerationCurrent(generation))
2758
+ return;
2510
2759
  const ackAfter = this.syncState(ns).ack;
2511
2760
  const target = targets.get(ns) ?? 0;
2512
2761
  if (ackAfter >= target || ackAfter <= ackBefore)
@@ -2514,6 +2763,7 @@ export class MessageDeliveryEngine {
2514
2763
  }
2515
2764
  }
2516
2765
  catch (exc) {
2766
+ failed = true;
2517
2767
  client._clientLog?.debug(`online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
2518
2768
  }
2519
2769
  finally {
@@ -2521,6 +2771,8 @@ export class MessageDeliveryEngine {
2521
2771
  targets.delete(ns);
2522
2772
  if (this.onlineUnreadHintOwners === owners)
2523
2773
  owners.delete(ns);
2774
+ if (this.isInlineGenerationCurrent(generation))
2775
+ this.finishBackgroundPull(ns, attemptAfterSeq, failed);
2524
2776
  }
2525
2777
  })());
2526
2778
  }
@@ -2529,6 +2781,7 @@ export class MessageDeliveryEngine {
2529
2781
  const groupId = String(data.group_id ?? '').trim();
2530
2782
  if (!groupId || client._sessionOptions?.background_sync === false)
2531
2783
  return;
2784
+ this.recordBackgroundPullDemand(`group_event:${groupId}`, positiveSafeSequenceHint(data.event_seq), true);
2532
2785
  const rawAfter = Number(data._online_hint_after_event_seq ?? 0);
2533
2786
  const afterSeq = Number.isSafeInteger(rawAfter) && rawAfter > 0 ? rawAfter : 0;
2534
2787
  client._safeAsync(this.fillGroupEventGap(groupId, {
@@ -2879,9 +3132,8 @@ export class MessageDeliveryEngine {
2879
3132
  async commitInlineMessage(ns, seq, event, payload, generation) {
2880
3133
  const client = this.runtime.client;
2881
3134
  const tracker = client._seqTracker;
2882
- const snapshot = typeof tracker.snapshotNamespace === 'function'
2883
- ? tracker.snapshotNamespace(ns)
2884
- : null;
3135
+ let snapshot = null;
3136
+ let trackerChanged = false;
2885
3137
  try {
2886
3138
  const selfSync = (event === 'message.received' && this.isSelfSentP2PMessage(payload))
2887
3139
  || (event === 'group.message_created' && this.isSelfSentGroupMessage(payload));
@@ -2892,6 +3144,8 @@ export class MessageDeliveryEngine {
2892
3144
  return false;
2893
3145
  if (!published)
2894
3146
  return false;
3147
+ snapshot = typeof tracker.snapshotNamespace === 'function' ? tracker.snapshotNamespace(ns) : null;
3148
+ trackerChanged = true;
2895
3149
  const needsPull = Boolean(tracker.onMessageSeq(ns, seq));
2896
3150
  const committed = this.syncState(ns);
2897
3151
  if (needsPull || committed.ack !== seq || committed.tail !== seq || committed.head !== seq) {
@@ -2905,8 +3159,8 @@ export class MessageDeliveryEngine {
2905
3159
  return true;
2906
3160
  }
2907
3161
  catch (exc) {
2908
- if (this.isInlineGenerationCurrent(generation) && typeof tracker.restoreNamespaceSnapshot === 'function') {
2909
- tracker.restoreNamespaceSnapshot(ns, snapshot);
3162
+ if (trackerChanged && this.isInlineGenerationCurrent(generation) && typeof tracker.restoreNamespaceSnapshot === 'function') {
3163
+ tracker._restoreNamespaceSnapshotPreservingPush(ns, snapshot);
2910
3164
  }
2911
3165
  client._clientLog.debug(`inline realtime consume rolled back: ns=${ns} seq=${seq} err=${formatDeliveryError(exc)}`);
2912
3166
  return false;
@@ -2995,7 +3249,7 @@ export class MessageDeliveryEngine {
2995
3249
  }
2996
3250
  }
2997
3251
  hasOutstandingRealtimeGap(ns) {
2998
- const state = this.syncState(ns);
3252
+ const state = this.prospectiveTailState(ns);
2999
3253
  // Tail 已建立可靠后缀窗口;后台续拉只针对尚未接上窗口的真实缺口。
3000
3254
  // maxSeen 可能只是 Tail 期间到达的通知,不能单独据此启动全量补洞。
3001
3255
  return state.ack < state.tail - 1;
@@ -3051,8 +3305,8 @@ export class MessageDeliveryEngine {
3051
3305
  }
3052
3306
  async runP2PRealtimeSync(ns, pushSeq, pageLimit = 50, forceTail = false) {
3053
3307
  const client = this.runtime.client;
3054
- const before = this.syncState(ns);
3055
- const order = forceTail ? ['tail'] : typeof client._seqTracker.planRealtimeSync === 'function'
3308
+ const before = this.prospectiveTailState(ns);
3309
+ const order = forceTail ? ['tail'] : this.pendingTailWindows.has(ns) ? ['forward'] : typeof client._seqTracker.planRealtimeSync === 'function'
3056
3310
  ? client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit)
3057
3311
  : (() => {
3058
3312
  const gap = pushSeq - before.ack;
@@ -3065,7 +3319,7 @@ export class MessageDeliveryEngine {
3065
3319
  let result = { messages: [], raw_count: 0 };
3066
3320
  let tailCompleted = false;
3067
3321
  const run = async (tail, background, maxPages, forceForward = false, bypassCoveredHead = false) => {
3068
- const live = this.syncState(ns);
3322
+ const live = this.prospectiveTailState(ns);
3069
3323
  if (tail && !bypassCoveredHead && pushSeq <= live.head)
3070
3324
  return { messages: [], raw_count: 0 };
3071
3325
  if (!tail && !forceForward && live.ack >= live.tail - 1)
@@ -3089,7 +3343,7 @@ export class MessageDeliveryEngine {
3089
3343
  };
3090
3344
  if (forceTail || order[0] === 'tail')
3091
3345
  result = await run(true, false, undefined, false, forceTail);
3092
- const state = this.syncState(ns);
3346
+ const state = this.prospectiveTailState(ns);
3093
3347
  const maxSeen = Number(client._seqTracker.getMaxSeenSeq(ns) || 0);
3094
3348
  // Tail 期间到达的新 Push 只记录在 pending Pull 上界,不写入 SeqTracker。
3095
3349
  const pendingUpper = Number(this.pendingP2pPullUpper?.get(ns) ?? 0);
@@ -3104,16 +3358,16 @@ export class MessageDeliveryEngine {
3104
3358
  || (client._sessionOptions?.background_sync !== false && backgroundWindowForward)) {
3105
3359
  const forwardTarget = Math.max(headForward ? Math.max(pushSeq, maxSeen) : 0, (tailForward || backgroundWindowForward) ? state.tail - 1 : 0, tailMissedDuringPull ? observedUpper : 0);
3106
3360
  const forwardMaxPages = this.forwardMaxPages(state.ack, forwardTarget, pageLimit);
3107
- result = await run(false, !(headForward || tailForward), forwardMaxPages, headForward || tailMissedDuringPull);
3361
+ result = await run(false, !(headForward || tailForward), forwardMaxPages, headForward || tailForward || tailMissedDuringPull);
3108
3362
  if (pendingThroughSeq > 0) {
3109
- const committedAck = this.syncState(ns).ack;
3363
+ const committedAck = this.prospectiveTailState(ns).ack;
3110
3364
  if (committedAck >= pendingThroughSeq)
3111
3365
  this.consumePendingPull(ns, pendingThroughSeq);
3112
3366
  else if (committedAck <= state.ack)
3113
3367
  this.markPendingPullNoProgress(ns, committedAck);
3114
3368
  }
3115
3369
  }
3116
- const finalState = this.syncState(ns);
3370
+ const finalState = this.prospectiveTailState(ns);
3117
3371
  if (tailCompleted && finalState.ack < finalState.tail - 1) {
3118
3372
  this.scheduleP2PRealtimeFullGapFill(ns);
3119
3373
  }
@@ -3123,8 +3377,8 @@ export class MessageDeliveryEngine {
3123
3377
  }
3124
3378
  async runGroupRealtimeSync(groupId, ns, pushSeq, pageLimit = 50, forceTail = false) {
3125
3379
  const client = this.runtime.client;
3126
- const before = this.syncState(ns);
3127
- const order = forceTail ? ['tail'] : typeof client._seqTracker.planRealtimeSync === 'function'
3380
+ const before = this.prospectiveTailState(ns);
3381
+ const order = forceTail ? ['tail'] : this.pendingTailWindows.has(ns) ? ['forward'] : typeof client._seqTracker.planRealtimeSync === 'function'
3128
3382
  ? client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit)
3129
3383
  : (() => {
3130
3384
  const gap = pushSeq - before.ack;
@@ -3137,7 +3391,7 @@ export class MessageDeliveryEngine {
3137
3391
  let result = { messages: [], raw_count: 0 };
3138
3392
  let tailCompleted = false;
3139
3393
  const run = async (tail, background, maxPages, forceForward = false, bypassCoveredHead = false) => {
3140
- const live = this.syncState(ns);
3394
+ const live = this.prospectiveTailState(ns);
3141
3395
  if (tail && !bypassCoveredHead && pushSeq <= live.head)
3142
3396
  return { messages: [], raw_count: 0 };
3143
3397
  if (!tail && !forceForward && live.ack >= live.tail - 1)
@@ -3161,7 +3415,7 @@ export class MessageDeliveryEngine {
3161
3415
  };
3162
3416
  if (forceTail || order[0] === 'tail')
3163
3417
  result = await run(true, false, undefined, false, forceTail);
3164
- const state = this.syncState(ns);
3418
+ const state = this.prospectiveTailState(ns);
3165
3419
  // 与 P2P 相同:真实完成 Tail 后,至多用一页 Forward 收敛 Tail
3166
3420
  // 期间新到达的序号;绝不重新规划或再执行 Tail。
3167
3421
  const maxSeen = Number(client._seqTracker.getMaxSeenSeq(ns) || 0);
@@ -3177,16 +3431,16 @@ export class MessageDeliveryEngine {
3177
3431
  || (client._sessionOptions?.background_sync !== false && backgroundWindowForward)) {
3178
3432
  const forwardTarget = Math.max(headForward ? Math.max(pushSeq, maxSeen) : 0, (tailForward || backgroundWindowForward) ? state.tail - 1 : 0, tailMissedDuringPull ? observedUpper : 0);
3179
3433
  const forwardMaxPages = this.forwardMaxPages(state.ack, forwardTarget, pageLimit);
3180
- result = await run(false, !(headForward || tailForward), forwardMaxPages, headForward || tailMissedDuringPull);
3434
+ result = await run(false, !(headForward || tailForward), forwardMaxPages, headForward || tailForward || tailMissedDuringPull);
3181
3435
  if (pendingThroughSeq > 0) {
3182
- const committedAck = this.syncState(ns).ack;
3436
+ const committedAck = this.prospectiveTailState(ns).ack;
3183
3437
  if (committedAck >= pendingThroughSeq)
3184
3438
  this.consumePendingPull(ns, pendingThroughSeq);
3185
3439
  else if (committedAck <= state.ack)
3186
3440
  this.markPendingPullNoProgress(ns, committedAck);
3187
3441
  }
3188
3442
  }
3189
- const finalState = this.syncState(ns);
3443
+ const finalState = this.prospectiveTailState(ns);
3190
3444
  if (tailCompleted && finalState.ack < finalState.tail - 1) {
3191
3445
  this.scheduleGroupRealtimeFullGapFill(groupId, ns);
3192
3446
  }
@@ -3302,22 +3556,30 @@ export class MessageDeliveryEngine {
3302
3556
  }
3303
3557
  return;
3304
3558
  }
3559
+ if (client._sessionOptions?.background_sync !== false)
3560
+ this.recordBackgroundPullDemand(ns, seq);
3305
3561
  const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
3306
3562
  if (pullActive || !this.beginRealtimeSync(ns)) {
3307
3563
  this.recordPendingPull(ns, seq);
3308
3564
  client._clientLog?.debug(`V2 group push queued behind in-flight realtime sync: ns=${ns} seq=${seq}`);
3309
3565
  return;
3310
3566
  }
3567
+ const generation = this.captureInlineGeneration();
3568
+ let failed = false;
3311
3569
  try {
3312
3570
  // namespace 串行区必须覆盖 Tail 与其后至多一次 Forward 的完整周期。
3313
3571
  await this.runGroupRealtimeSync(groupId, ns, seq, 50, false);
3314
3572
  }
3315
3573
  catch (exc) {
3574
+ failed = true;
3316
3575
  client._clientLog.warn(`V2 group push auto-sync failed: group=${groupId} err=${formatDeliveryError(exc)}`);
3317
3576
  }
3318
3577
  finally {
3319
- this.endRealtimeSync(ns);
3320
- this.schedulePendingPullIfNeeded(ns, 'push-auto-pull-complete');
3578
+ if (this.isInlineGenerationCurrent(generation)) {
3579
+ this.endRealtimeSync(ns);
3580
+ this.schedulePendingPullIfNeeded(ns, 'push-auto-pull-complete');
3581
+ this.finishBackgroundPull(ns, before.ack, failed);
3582
+ }
3321
3583
  }
3322
3584
  }
3323
3585
  async onV2PushNotification(data) {
@@ -3390,31 +3652,60 @@ export class MessageDeliveryEngine {
3390
3652
  }
3391
3653
  return;
3392
3654
  }
3655
+ if (client._sessionOptions?.background_sync !== false)
3656
+ this.recordBackgroundPullDemand(ns, pushSeq);
3393
3657
  const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
3394
3658
  if (pullActive || !this.beginRealtimeSync(ns)) {
3395
3659
  this.recordPendingPull(ns, pushSeq);
3396
3660
  client._clientLog?.debug(`V2 P2P push queued behind in-flight realtime sync: ns=${ns} seq=${pushSeq}`);
3397
3661
  return;
3398
3662
  }
3663
+ const generation = this.captureInlineGeneration();
3664
+ let failed = false;
3399
3665
  try {
3400
3666
  await this.runP2PRealtimeSync(ns, pushSeq, 50, false);
3401
3667
  }
3402
3668
  catch (exc) {
3669
+ failed = true;
3403
3670
  client._clientLog.warn(`V2 P2P push auto-sync failed: ${formatDeliveryError(exc)}`);
3404
3671
  }
3405
3672
  finally {
3406
- this.endRealtimeSync(ns);
3407
- this.schedulePendingPullIfNeeded(ns, 'push-auto-pull-complete');
3673
+ if (this.isInlineGenerationCurrent(generation)) {
3674
+ this.endRealtimeSync(ns);
3675
+ this.schedulePendingPullIfNeeded(ns, 'push-auto-pull-complete');
3676
+ this.finishBackgroundPull(ns, before.ack, failed);
3677
+ }
3408
3678
  }
3409
3679
  }
3410
3680
  enqueueOnlineUnreadP2pHint(data) {
3411
3681
  const client = this.runtime.client;
3412
- if (!client._aid || !client._v2Session)
3682
+ if (!client._aid || !client._v2Session || client._sessionOptions?.background_sync === false)
3413
3683
  return;
3414
3684
  const ns = `p2p:${client._aid}`;
3415
- client._safeAsync(this.runP2PForwardRecovery(ns, 50, true).catch((exc) => {
3416
- client._clientLog?.debug(`P2P online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
3417
- }));
3685
+ const generation = this.captureInlineGeneration();
3686
+ const afterSeq = this.syncState(ns).ack;
3687
+ this.recordBackgroundPullDemand(ns, positiveSafeSequenceHint(data.seq), true);
3688
+ const owners = this.onlineUnreadHintOwners ?? new Set();
3689
+ this.onlineUnreadHintOwners = owners;
3690
+ if (owners.has(ns))
3691
+ return;
3692
+ owners.add(ns);
3693
+ client._safeAsync((async () => {
3694
+ let failed = false;
3695
+ try {
3696
+ await this.runP2PForwardRecovery(ns, 50, true);
3697
+ }
3698
+ catch (exc) {
3699
+ failed = true;
3700
+ client._clientLog?.debug(`P2P online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
3701
+ }
3702
+ finally {
3703
+ if (this.onlineUnreadHintOwners === owners)
3704
+ owners.delete(ns);
3705
+ if (this.isInlineGenerationCurrent(generation))
3706
+ this.finishBackgroundPull(ns, afterSeq, failed);
3707
+ }
3708
+ })());
3418
3709
  }
3419
3710
  async restoreSeqTrackerState() {
3420
3711
  const client = this.runtime.client;
@@ -3803,7 +4094,7 @@ export class MessageDeliveryEngine {
3803
4094
  }
3804
4095
  return params;
3805
4096
  }
3806
- async drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true, batch, source = 'pull') {
4097
+ async drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true, batch, source = 'pull', throughSeq) {
3807
4098
  const client = this.runtime.client;
3808
4099
  const pullScoped = this.isPullScopedSource(source);
3809
4100
  if (pullScoped)
@@ -3811,7 +4102,8 @@ export class MessageDeliveryEngine {
3811
4102
  const queue = client._pendingOrderedMsgs.get(ns);
3812
4103
  if (!queue || queue.size === 0)
3813
4104
  return;
3814
- const contig = client._seqTracker.getContiguousSeq(ns);
4105
+ const generation = this.captureInlineGeneration();
4106
+ const contig = throughSeq ?? client._seqTracker.getContiguousSeq(ns);
3815
4107
  const ready = [...queue.keys()]
3816
4108
  .filter((seq) => seq <= contig && (beforeSeq === undefined || seq < beforeSeq))
3817
4109
  .sort((a, b) => a - b);
@@ -3822,9 +4114,12 @@ export class MessageDeliveryEngine {
3822
4114
  if (pullScoped)
3823
4115
  this.ensurePullOperationCurrent(ns);
3824
4116
  const item = queue.get(seq);
3825
- queue.delete(seq);
3826
- if (!item || client._pushedSeqs.get(ns)?.has(seq))
4117
+ if (!item)
3827
4118
  continue;
4119
+ if (client._pushedSeqs.get(ns)?.has(seq)) {
4120
+ queue.delete(seq);
4121
+ continue;
4122
+ }
3828
4123
  const itemSource = item.source ?? source;
3829
4124
  let itemBatch = batch;
3830
4125
  if ((itemSource === 'push' || itemSource === 'inline_push')
@@ -3836,8 +4131,13 @@ export class MessageDeliveryEngine {
3836
4131
  }
3837
4132
  }
3838
4133
  await this.publishOrderedQueueItem(ns, item.event, seq, item.payload, pullResponse, itemSource, itemBatch);
4134
+ if (!this.isInlineGenerationCurrent(generation))
4135
+ throw new Error('pull invalidated');
3839
4136
  if (pullScoped)
3840
4137
  this.ensurePullOperationCurrent(ns);
4138
+ queue.delete(seq);
4139
+ if (item.event === 'group.changed' && isJsonObject(item.payload) && item.payload.action === 'dissolved')
4140
+ continue;
3841
4141
  this.markPublishedSeq(ns, seq);
3842
4142
  delivered = true;
3843
4143
  }
@@ -3858,6 +4158,7 @@ export class MessageDeliveryEngine {
3858
4158
  }
3859
4159
  async publishOrderedMessage(event, ns, seq, payload, source = 'push', operationBatch) {
3860
4160
  const client = this.runtime.client;
4161
+ const generation = this.captureInlineGeneration();
3861
4162
  const ownsBatch = !operationBatch && (source === 'push' || source === 'inline_push');
3862
4163
  const batch = operationBatch ?? (ownsBatch ? this.createDeliveryChangeBatch(source) : undefined);
3863
4164
  try {
@@ -3887,10 +4188,12 @@ export class MessageDeliveryEngine {
3887
4188
  if (client._pushedSeqs.get(ns)?.has(seqNum))
3888
4189
  return false;
3889
4190
  const queue = client._pendingOrderedMsgs.get(ns);
4191
+ await this.publishOrderedQueueItem(ns, event, seqNum, payload, false, source, batch);
4192
+ if (!this.isInlineGenerationCurrent(generation))
4193
+ throw new Error('pull invalidated');
3890
4194
  queue?.delete(seqNum);
3891
4195
  if (queue && queue.size === 0)
3892
4196
  client._pendingOrderedMsgs.delete(ns);
3893
- await this.publishOrderedQueueItem(ns, event, seqNum, payload, false, source, batch);
3894
4197
  this.markPublishedSeq(ns, seqNum);
3895
4198
  await this.drainOrderedMessages(ns, undefined, false, true, batch, source);
3896
4199
  if (!client._pendingOrderedMsgs.get(ns))
@@ -3904,6 +4207,7 @@ export class MessageDeliveryEngine {
3904
4207
  }
3905
4208
  async publishPulledMessage(event, ns, seq, payload, persist = true, source = 'pull', operationBatch) {
3906
4209
  const client = this.runtime.client;
4210
+ const generation = this.captureInlineGeneration();
3907
4211
  const ownsBatch = !operationBatch && (source === 'push' || source === 'inline_push');
3908
4212
  const batch = operationBatch ?? (ownsBatch ? this.createDeliveryChangeBatch(source) : undefined);
3909
4213
  const pullScoped = this.isPullScopedSource(source);
@@ -3938,19 +4242,26 @@ export class MessageDeliveryEngine {
3938
4242
  await this.drainOrderedMessages(ns, seqNum, false, persist, batch, source);
3939
4243
  if (pullScoped)
3940
4244
  this.ensurePullOperationCurrent(ns);
3941
- queue?.delete(seqNum);
3942
- if (queue && queue.size === 0)
3943
- client._pendingOrderedMsgs.delete(ns);
3944
4245
  if (event === 'message.recalled') {
3945
4246
  const published = await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seqNum, payload, source));
4247
+ if (!this.isInlineGenerationCurrent(generation))
4248
+ throw new Error('pull invalidated');
3946
4249
  if (pullScoped)
3947
4250
  this.ensurePullOperationCurrent(ns);
4251
+ queue?.delete(seqNum);
4252
+ if (queue && queue.size === 0)
4253
+ client._pendingOrderedMsgs.delete(ns);
3948
4254
  this.markPublishedSeq(ns, seqNum);
3949
4255
  return published;
3950
4256
  }
3951
4257
  await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, source, ns, batch));
4258
+ if (!this.isInlineGenerationCurrent(generation))
4259
+ throw new Error('pull invalidated');
3952
4260
  if (pullScoped)
3953
4261
  this.ensurePullOperationCurrent(ns);
4262
+ queue?.delete(seqNum);
4263
+ if (queue && queue.size === 0)
4264
+ client._pendingOrderedMsgs.delete(ns);
3954
4265
  this.markPublishedSeq(ns, seqNum);
3955
4266
  return true;
3956
4267
  }