@agentunion/fastaun 0.5.21 → 0.5.22

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 (71) hide show
  1. package/CHANGELOG.md +58 -1
  2. package/_packed_docs/CHANGELOG.md +58 -1
  3. package/_packed_docs/INDEX.md +465 -453
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +168 -164
  5. 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 +307 -67
  6. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +41 -15
  7. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +5 -5
  8. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +30 -8
  9. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +669 -666
  10. package/_packed_docs/sdk/0.5.22-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +212 -0
  11. package/_packed_docs/sdk/0.5.22-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +199 -0
  12. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +16 -0
  13. package/_packed_docs/sdk/09-group-rpc-manual.md +10 -0
  14. package/_packed_docs/sdk/09-storage-rpc-manual.md +64 -7
  15. package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +3 -1
  16. package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +2 -0
  17. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +138 -134
  18. package/_packed_docs/sdk/INDEX.md +255 -239
  19. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +939 -930
  20. 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 +312 -363
  21. 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 +322 -553
  22. 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 +4 -4
  23. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +484 -480
  24. 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 +190 -97
  25. 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 +158 -127
  26. package/dist/agent-md.d.ts +1 -1
  27. package/dist/agent-md.js +11 -4
  28. package/dist/agent-md.js.map +1 -1
  29. package/dist/auth.d.ts +1 -0
  30. package/dist/auth.js +18 -2
  31. package/dist/auth.js.map +1 -1
  32. package/dist/client/delivery.d.ts +28 -0
  33. package/dist/client/delivery.js +399 -44
  34. package/dist/client/delivery.js.map +1 -1
  35. package/dist/client/group-state.js +3 -0
  36. package/dist/client/group-state.js.map +1 -1
  37. package/dist/client/rpc-pipeline.js +28 -2
  38. package/dist/client/rpc-pipeline.js.map +1 -1
  39. package/dist/client/v2-e2ee.js +109 -39
  40. package/dist/client/v2-e2ee.js.map +1 -1
  41. package/dist/client.js +38 -10
  42. package/dist/client.js.map +1 -1
  43. package/dist/group-fs.d.ts +1 -0
  44. package/dist/group-fs.js +37 -19
  45. package/dist/group-fs.js.map +1 -1
  46. package/dist/org-protocol.d.ts +1 -0
  47. package/dist/org-protocol.js +54 -7
  48. package/dist/org-protocol.js.map +1 -1
  49. package/dist/ownership-v21.d.ts +21 -3
  50. package/dist/ownership-v21.js +135 -37
  51. package/dist/ownership-v21.js.map +1 -1
  52. package/dist/ownership-workflow.d.ts +7 -2
  53. package/dist/ownership-workflow.js +173 -17
  54. package/dist/ownership-workflow.js.map +1 -1
  55. package/dist/storage/lowlevel.d.ts +6 -0
  56. package/dist/storage/lowlevel.js +24 -7
  57. package/dist/storage/lowlevel.js.map +1 -1
  58. package/dist/storage/read-cache.d.ts +70 -0
  59. package/dist/storage/read-cache.js +625 -0
  60. package/dist/storage/read-cache.js.map +1 -0
  61. package/dist/storage/vfs.d.ts +2 -0
  62. package/dist/storage/vfs.js +50 -15
  63. package/dist/storage/vfs.js.map +1 -1
  64. package/dist/tools/cross-sdk-agent.d.ts +21 -1
  65. package/dist/tools/cross-sdk-agent.js +301 -19
  66. package/dist/tools/cross-sdk-agent.js.map +1 -1
  67. package/dist/transport.js +6 -2
  68. package/dist/transport.js.map +1 -1
  69. package/dist/version.d.ts +1 -1
  70. package/dist/version.js +1 -1
  71. package/package.json +1 -1
@@ -2,6 +2,7 @@ import { _INTERNAL_MESSAGE_FLOW_LIMITS, slotIsolationKey } from '../config.js';
2
2
  import { parseFileFrontmatter } from '../agent-md-schema.js';
3
3
  import { AsyncLocalStorage } from 'node:async_hooks';
4
4
  import { trafficLogContext, withLogContext } from '../logger.js';
5
+ import { normalizeGroupAid } from '../group-id.js';
5
6
  import { ConnectionState, isJsonObject } from '../types.js';
6
7
  import { validateGroupAIDFormat } from '../validators.js';
7
8
  import { hasGroupMentionModeField, normalizeGroupMentionMode } from './mention-mode.js';
@@ -12,6 +13,24 @@ const GROUP_RECALL_SEEN_LIMIT = 10_000;
12
13
  const VOLATILE_SEEN_LIMIT = 10_000;
13
14
  const SEQ_TRACKER_PERSIST_FLUSH_DELAY_MS = 200;
14
15
  const BACKGROUND_PULL_RETRY_DELAY_MS = 250;
16
+ function isTerminalGroupPullError(error) {
17
+ if (!error || typeof error !== 'object')
18
+ return false;
19
+ const failure = error;
20
+ if (typeof failure.data?.retryable === 'boolean')
21
+ return !failure.data.retryable;
22
+ if (failure.retryable === true)
23
+ return false;
24
+ const message = String(failure.message ?? '').trim().toLowerCase();
25
+ if (['timeout', 'deadline exceeded', 'gateway service degraded', 'certificate not loaded',
26
+ 'long_connection_already_exists', 'long connection already exists'].some((text) => message.includes(text)))
27
+ return false;
28
+ return [-32001, -32003, -32004, -32600, -32601, -32602].includes(failure.code ?? 0)
29
+ || (typeof failure.code === 'number' && failure.code >= -33009 && failure.code <= -33001)
30
+ || ['banned members', 'blocked members', 'member is banned', 'member is blocked',
31
+ 'group is not active', 'group is dissolved', 'group is suspended',
32
+ 'group not found', 'member not found', 'not a member'].some((text) => message.includes(text));
33
+ }
15
34
  const DELIVERY_CHANGED_MESSAGE_EVENTS = new Set([
16
35
  'message.received',
17
36
  'group.message_created',
@@ -115,8 +134,12 @@ export class MessageDeliveryEngine {
115
134
  seqTrackerNamespaceGenerations = new Map();
116
135
  peerRecoveryTasks = new Map();
117
136
  peerRecoveryNeeds = new Map();
137
+ peerForwardNeeds = new Map();
138
+ peerPushVersions = new Map();
139
+ peerCatalogVersion = null;
118
140
  peerPullHints = new Map();
119
141
  peerPullConfirmedThrough = new Map();
142
+ peerServerConfirmedAcks = new Map();
120
143
  peerVolatileCursors = new Map();
121
144
  settledDeliveries = new Map();
122
145
  deliveryInflight = new Map();
@@ -124,9 +147,100 @@ export class MessageDeliveryEngine {
124
147
  applicationClaims = new Map();
125
148
  volatileCompletions = new Map();
126
149
  peerPageChains = new Map();
150
+ automaticPullFailures = new Map();
151
+ terminalGroupPulls = new Map();
152
+ automaticPullOperations = new WeakSet();
153
+ automaticPullContext = new AsyncLocalStorage();
154
+ automaticPullAbort = new AbortController();
127
155
  constructor(runtime) {
128
156
  this.runtime = runtime;
129
157
  }
158
+ resetAutomaticPullBackoff() {
159
+ this.automaticPullAbort.abort();
160
+ this.automaticPullAbort = new AbortController();
161
+ this.automaticPullFailures.clear();
162
+ this.terminalGroupPulls.clear();
163
+ }
164
+ groupPullNamespace(namespace) {
165
+ const groupId = namespace.startsWith('group_event:') ? namespace.slice('group_event:'.length)
166
+ : namespace.startsWith('group:') ? namespace.slice('group:'.length) : '';
167
+ return groupId ? `group:${normalizeGroupAid(groupId)}` : '';
168
+ }
169
+ automaticGroupPullStopped(namespace) {
170
+ return this.terminalGroupPulls.has(this.groupPullNamespace(namespace));
171
+ }
172
+ observeSuccessfulGroupPull(namespace) {
173
+ if (!this.automaticPullContext.getStore())
174
+ this.terminalGroupPulls.delete(this.groupPullNamespace(namespace));
175
+ }
176
+ stopAutomaticGroupPull(namespace, error) {
177
+ const canonical = this.groupPullNamespace(namespace);
178
+ if (!canonical)
179
+ return;
180
+ this.terminalGroupPulls.set(canonical, error);
181
+ for (const pending of [this.automaticPullFailures, this.backgroundPullNeeds, this.pendingGroupPullUpper, this.onlineUnreadHintTargets]) {
182
+ for (const ns of pending?.keys() ?? []) {
183
+ if (this.groupPullNamespace(ns) === canonical)
184
+ pending.delete(ns);
185
+ }
186
+ }
187
+ }
188
+ automaticPullRetryAt(namespace, operation) {
189
+ return this.automaticPullOperations.has(operation) ? this.automaticPullFailures.get(namespace)?.retryAt ?? 0 : 0;
190
+ }
191
+ runPeerPullAttempt(namespace, operation, schedule) {
192
+ return this.automaticPullContext.getStore() ? this.runAutomaticPull(namespace, operation, schedule) : schedule(operation);
193
+ }
194
+ async runAutomaticPull(namespace, operation, schedule) {
195
+ this.ensurePeerContext();
196
+ if (this.automaticGroupPullStopped(namespace))
197
+ throw this.terminalGroupPulls.get(this.groupPullNamespace(namespace));
198
+ const nested = this.automaticPullContext.getStore() === namespace;
199
+ const generation = this.captureInlineAckGeneration();
200
+ const signal = this.automaticPullAbort.signal;
201
+ const client = this.runtime.client;
202
+ while ((this.automaticPullFailures.get(namespace)?.retryAt ?? 0) > Date.now()) {
203
+ const delay = this.automaticPullFailures.get(namespace).retryAt - Date.now();
204
+ await new Promise((resolve, reject) => {
205
+ const abort = () => {
206
+ clearTimeout(timer);
207
+ reject(new Error('pull invalidated'));
208
+ };
209
+ const timer = setTimeout(() => { signal.removeEventListener('abort', abort); resolve(); }, delay);
210
+ signal.addEventListener('abort', abort, { once: true });
211
+ });
212
+ }
213
+ if (signal.aborted || generation !== this.captureInlineAckGeneration() || client._closing || client.state !== ConnectionState.READY) {
214
+ throw new Error('pull invalidated');
215
+ }
216
+ const attempt = async () => {
217
+ if (this.automaticGroupPullStopped(namespace))
218
+ throw this.terminalGroupPulls.get(this.groupPullNamespace(namespace));
219
+ const previousFailure = this.automaticPullFailures.get(namespace);
220
+ try {
221
+ const result = await this.automaticPullContext.run(namespace, operation);
222
+ if (!nested && !signal.aborted && generation === this.captureInlineAckGeneration())
223
+ this.automaticPullFailures.delete(namespace);
224
+ return result;
225
+ }
226
+ catch (error) {
227
+ if (!signal.aborted && generation === this.captureInlineAckGeneration()
228
+ && this.groupPullNamespace(namespace) && isTerminalGroupPullError(error)) {
229
+ this.stopAutomaticGroupPull(namespace, error);
230
+ throw error;
231
+ }
232
+ const currentFailure = this.automaticPullFailures.get(namespace);
233
+ if ((!currentFailure || currentFailure === previousFailure)
234
+ && !signal.aborted && generation === this.captureInlineAckGeneration() && !client._closing && client.state === ConnectionState.READY) {
235
+ const delay = Math.min(30_000, (this.automaticPullFailures.get(namespace)?.delay ?? 500) * 2);
236
+ this.automaticPullFailures.set(namespace, { delay, retryAt: Date.now() + delay });
237
+ }
238
+ throw error;
239
+ }
240
+ };
241
+ this.automaticPullOperations.add(attempt);
242
+ return await (schedule ? schedule(attempt) : attempt());
243
+ }
130
244
  currentApplicationPage() {
131
245
  const page = this.applicationPage.getStore();
132
246
  return page?.collecting && page.batching ? page : undefined;
@@ -143,6 +257,10 @@ export class MessageDeliveryEngine {
143
257
  snapshotApplicationHandlers() {
144
258
  return this.runtime.client._dispatcher._snapshotApplicationHandlers?.(['message.batch', 'group.message_batch', 'group.event_batch']);
145
259
  }
260
+ hasP2pApplicationSubscription() {
261
+ const handlers = this.runtime.client._dispatcher._snapshotApplicationHandlers?.(['message.received', 'message.recalled', 'message.batch']);
262
+ return !handlers || [...handlers.values()].some((selected) => selected.length > 0);
263
+ }
146
264
  async runApplicationPage(namespace, source, operation, reservation, snapshot) {
147
265
  const dispatcher = this.runtime.client._dispatcher;
148
266
  if (typeof dispatcher._snapshotApplicationHandlers !== 'function')
@@ -288,6 +406,7 @@ export class MessageDeliveryEngine {
288
406
  const client = this.runtime.client;
289
407
  const context = JSON.stringify([client._aid ?? '', client._deviceId ?? '', slotIsolationKey(client._slotId)]);
290
408
  if (context !== this.peerContext || client._seqTracker !== this.peerTracker) {
409
+ this.resetAutomaticPullBackoff();
291
410
  this.peerContext = context;
292
411
  this.peerTracker = client._seqTracker;
293
412
  this.peerContextGeneration += 1;
@@ -297,8 +416,12 @@ export class MessageDeliveryEngine {
297
416
  this.seqTrackerNamespaceGenerations.clear();
298
417
  this.peerVolatileCursors.clear();
299
418
  this.peerRecoveryNeeds.clear();
419
+ this.peerForwardNeeds.clear();
420
+ this.peerPushVersions.clear();
421
+ this.peerCatalogVersion = null;
300
422
  this.peerPullHints.clear();
301
423
  this.peerPullConfirmedThrough.clear();
424
+ this.peerServerConfirmedAcks.clear();
302
425
  this.peerRecoveryTasks.clear();
303
426
  this.settledDeliveries.clear();
304
427
  this.deliveryInflight.clear();
@@ -373,7 +496,14 @@ export class MessageDeliveryEngine {
373
496
  }
374
497
  peerCommittedAck(namespace) {
375
498
  this.ensurePeerContext();
376
- return Math.min(this.committedSeqTrackerStates.get(namespace)?.ack ?? 0, this.syncState(namespace).ack);
499
+ const committed = this.committedSeqTrackerStates.get(namespace);
500
+ return committed ? Math.min(committed.ack, this.syncState(namespace).ack) : 0;
501
+ }
502
+ observePeerServerAck(namespace, sequence) {
503
+ this.ensurePeerContext();
504
+ if (!namespace.startsWith('p2p.peer.v1/') || !Number.isSafeInteger(sequence) || sequence < 0)
505
+ return;
506
+ this.peerServerConfirmedAcks.set(namespace, Math.max(this.peerServerConfirmedAcks.get(namespace) ?? 0, sequence));
377
507
  }
378
508
  rememberCommittedSeqState(namespace) {
379
509
  this.ensurePeerContext();
@@ -461,7 +591,16 @@ export class MessageDeliveryEngine {
461
591
  }
462
592
  for (const row of rows) {
463
593
  const ns = this.peerNamespace(String(row.sender_aid));
594
+ if (row.volatile_has_more === true || (nonNegativeSafeSequenceHint(row.head_seq) ?? 0) > Math.max(this.syncState(ns).head, client._seqTracker.getMaxSeenSeq(ns))) {
595
+ this.peerPushVersions.set(ns, {});
596
+ }
597
+ const head = nonNegativeSafeSequenceHint(row.head_seq);
598
+ if (head !== null)
599
+ client._seqTracker.updateMaxSeen(ns, head);
464
600
  const hint = nonNegativeSafeSequenceHint(row.next_pending_seq);
601
+ const serverAck = nonNegativeSafeSequenceHint(row.server_ack_seq);
602
+ if (serverAck !== null)
603
+ this.observePeerServerAck(ns, serverAck);
465
604
  const confirmed = Math.max(this.peerPullConfirmedThrough.get(ns) ?? 0, nonNegativeSafeSequenceHint(row.server_ack_seq) ?? nonNegativeSafeSequenceHint(row.initial_after_seq) ?? 0);
466
605
  this.peerPullConfirmedThrough.set(ns, confirmed);
467
606
  const current = this.peerPullHints.get(ns);
@@ -478,8 +617,40 @@ export class MessageDeliveryEngine {
478
617
  client._dispatcher.enqueue('message.peers_snapshot', snapshot, () => context === this.ensurePeerContext() && generation === this.captureInlineAckGeneration());
479
618
  return snapshot;
480
619
  }
620
+ capturePeerTailVersion(senderAid) {
621
+ const context = this.ensurePeerContext();
622
+ return { context, push: this.peerPushVersions.get(this.peerNamespace(senderAid)), catalog: this.peerCatalogVersion };
623
+ }
624
+ isPeerTailCurrent(senderAid, version, committedAck) {
625
+ const namespace = this.peerNamespace(senderAid);
626
+ return version.context === this.ensurePeerContext() && this.peerPushVersions.get(namespace) === version.push
627
+ && this.peerCatalogVersion === version.catalog && this.runtime.client._seqTracker.getMaxSeenSeq(namespace) <= committedAck
628
+ && this.peerPullPriority(namespace) <= committedAck && this.syncState(namespace).head <= committedAck;
629
+ }
630
+ shouldTailPeer(namespace, head, limit, expectedForwardNeed) {
631
+ const state = this.syncState(namespace);
632
+ const knownHead = Math.max(head, state.head, this.runtime.client._seqTracker.getMaxSeenSeq(namespace), this.peerPullPriority(namespace));
633
+ return this.peerOwnType() === 'human' && (!this.peerForwardNeeds.has(namespace) || this.peerForwardNeeds.get(namespace) === expectedForwardNeed) && state.ack >= state.head
634
+ && Number.isSafeInteger(limit) && limit > 0 && knownHead - this.peerCommittedAck(namespace) > limit;
635
+ }
636
+ canClosePeerTail(namespace, tail, pushVersion, catalogVersion, expectedNeed) {
637
+ const head = nonNegativeSafeSequenceHint(tail.head_seq);
638
+ const committedAck = this.peerCommittedAck(namespace);
639
+ const need = this.peerRecoveryNeeds.get(this.peerSender(namespace));
640
+ return tail.complete === true && tail.volatile_has_more === false && tail.pending_ack_seq === 0
641
+ && head !== null && committedAck >= head && this.syncState(namespace).head <= committedAck
642
+ && (this.peerServerConfirmedAcks.get(namespace) ?? 0) >= committedAck
643
+ && this.runtime.client._seqTracker.getMaxSeenSeq(namespace) <= committedAck
644
+ && this.peerPullPriority(namespace) <= committedAck
645
+ && this.peerPushVersions.get(namespace) === pushVersion
646
+ && this.peerCatalogVersion === catalogVersion
647
+ && (!need || (need === expectedNeed && !need.pending && need.head <= committedAck));
648
+ }
481
649
  async pullPeers(params = {}, background = false) {
482
650
  const client = this.runtime.client;
651
+ const context = this.ensurePeerContext();
652
+ const generation = this.captureInlineAckGeneration();
653
+ const isCurrent = () => context === this.ensurePeerContext() && generation === this.captureInlineAckGeneration();
483
654
  const sender = String(params.sender_aid ?? '').trim();
484
655
  if (!sender && ('after_seq' in params || 'after_volatile_cursor' in params || 'window_mode' in params || params.force === true))
485
656
  throw new Error('explicit peer is required for after_seq, after_volatile_cursor, window_mode or force');
@@ -488,30 +659,88 @@ export class MessageDeliveryEngine {
488
659
  const budget = { remaining: Math.max(1, Math.trunc(Number(params.max_pages ?? 100) || 100)) };
489
660
  const results = await Promise.all(rows.map(async (row) => {
490
661
  const peer = String(row.sender_aid);
662
+ const peerPullVersion = this.capturePeerTailVersion(peer);
491
663
  const ns = await this.initializePeer(peer);
492
664
  try {
493
- const state = this.syncState(ns);
494
- if (!params.window_mode && params.force !== true && this.peerOwnType() === 'human' && state.head <= state.ack && budget.remaining > 0) {
495
- const tail = await client._v2E2EE.pullPeerV2(peer, { ...params, window_mode: 'tail', budget, background });
665
+ if (!isCurrent())
666
+ throw new Error('pull invalidated');
667
+ let forwardNeed = this.peerForwardNeeds.get(ns);
668
+ if (!params.window_mode && params.force !== true && this.shouldTailPeer(ns, Number(row.head_seq ?? 0), Number(params.limit ?? _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize)) && budget.remaining > 0) {
669
+ forwardNeed = {};
670
+ this.peerForwardNeeds.set(ns, forwardNeed);
671
+ const pushVersion = this.peerPushVersions.get(ns);
672
+ const catalogVersion = this.peerCatalogVersion;
673
+ const autoTail = { head: Number(row.head_seq ?? 0), forwardNeed, forward: false };
674
+ const tail = await client._v2E2EE.pullPeerV2(peer, { ...params, window_mode: 'tail', budget, background, peerPullVersion, autoTail });
675
+ if (!isCurrent())
676
+ throw new Error('pull invalidated');
677
+ if (autoTail.forward) {
678
+ const notified = this.peerPushVersions.get(ns) !== pushVersion || this.peerCatalogVersion !== catalogVersion;
679
+ if (notified)
680
+ this.peerForwardNeeds.set(ns, {});
681
+ else if (tail.complete === true && this.peerForwardNeeds.get(ns) === forwardNeed)
682
+ this.peerForwardNeeds.delete(ns);
683
+ return { ...tail, complete: tail.complete === true && !notified };
684
+ }
685
+ if (this.canClosePeerTail(ns, tail, pushVersion, catalogVersion)) {
686
+ if (this.peerForwardNeeds.get(ns) === forwardNeed)
687
+ this.peerForwardNeeds.delete(ns);
688
+ return tail;
689
+ }
496
690
  if (!budget.remaining) {
497
- if (row.volatile_has_more)
498
- this.schedulePeerRecovery(peer, Number(tail.head_seq ?? 0), true);
499
- return { ...tail, complete: tail.complete && !row.volatile_has_more };
691
+ this.schedulePeerRecovery(peer, Number(tail.head_seq ?? 0), true);
692
+ return { ...tail, complete: false };
500
693
  }
694
+ forwardNeed = this.peerForwardNeeds.get(ns);
695
+ const forwardPushVersion = this.peerPushVersions.get(ns);
696
+ const forwardCatalogVersion = this.peerCatalogVersion;
501
697
  const forward = await client._v2E2EE.pullPeerV2(peer, { ...params, budget, background });
698
+ if (!isCurrent())
699
+ throw new Error('pull invalidated');
700
+ const notified = this.peerPushVersions.get(ns) !== forwardPushVersion || this.peerCatalogVersion !== forwardCatalogVersion;
701
+ if (notified)
702
+ this.peerForwardNeeds.set(ns, {});
703
+ else if (forward.complete === true && this.peerForwardNeeds.get(ns) === forwardNeed)
704
+ this.peerForwardNeeds.delete(ns);
502
705
  const messages = [...new Map([...tail.messages, ...forward.messages].map((message) => [message.seq ?? message.volatile_cursor, message])).values()];
503
- return { ...forward, messages };
706
+ return { ...forward, messages, complete: forward.complete === true && !notified };
504
707
  }
505
- return await client._v2E2EE.pullPeerV2(peer, { ...params, budget, background });
708
+ const pushVersion = this.peerPushVersions.get(ns);
709
+ const catalogVersion = this.peerCatalogVersion;
710
+ const result = await client._v2E2EE.pullPeerV2(peer, { ...params, budget, background, peerPullVersion });
711
+ if (!isCurrent())
712
+ throw new Error('pull invalidated');
713
+ if (params.window_mode === 'tail') {
714
+ const complete = this.canClosePeerTail(ns, result, pushVersion, catalogVersion);
715
+ if (!complete) {
716
+ this.peerForwardNeeds.set(ns, {});
717
+ this.schedulePeerRecovery(peer, Number(result.head_seq ?? 0), true);
718
+ }
719
+ else if (this.peerForwardNeeds.get(ns) === forwardNeed)
720
+ this.peerForwardNeeds.delete(ns);
721
+ return { ...result, complete };
722
+ }
723
+ if (params.force !== true && (this.peerPushVersions.get(ns) !== pushVersion || this.peerCatalogVersion !== catalogVersion)) {
724
+ this.peerForwardNeeds.set(ns, {});
725
+ return { ...result, complete: false };
726
+ }
727
+ if (params.force !== true && result.complete === true && this.peerForwardNeeds.get(ns) === forwardNeed)
728
+ this.peerForwardNeeds.delete(ns);
729
+ return result;
506
730
  }
507
731
  catch (error) {
508
- this.schedulePeerRecovery(peer, Number(row.head_seq ?? 0), Boolean(row.volatile_has_more));
509
- return { sender_aid: peer, messages: [], complete: false, ack_seq: this.syncState(ns).ack, error: error instanceof Error ? error.message : String(error) };
732
+ if (isCurrent())
733
+ this.schedulePeerRecovery(peer, Number(row.head_seq ?? 0), Boolean(row.volatile_has_more));
734
+ return { sender_aid: peer, messages: [], complete: false, ack_seq: isCurrent() ? this.syncState(ns).ack : 0, error: error instanceof Error ? error.message : String(error) };
510
735
  }
511
736
  }));
512
737
  return { cursor_scope: 'peer', peers: results, messages: results.flatMap((row) => row.messages), partial: results.some((row) => !row.complete) };
513
738
  }
514
739
  refreshPendingPeers() {
740
+ this.ensurePeerContext();
741
+ this.peerCatalogVersion = {};
742
+ if (!this.hasP2pApplicationSubscription())
743
+ return;
515
744
  const client = this.runtime.client;
516
745
  if (client._sessionOptions?.background_sync === false || !client._aid || client._closing || client.state !== ConnectionState.READY)
517
746
  return;
@@ -521,9 +750,13 @@ export class MessageDeliveryEngine {
521
750
  return;
522
751
  const generation = this.captureInlineAckGeneration();
523
752
  const task = (async () => {
524
- while (this.peerCatalogDirty && generation === this.captureInlineAckGeneration()) {
753
+ while (this.peerCatalogDirty && generation === this.captureInlineAckGeneration() && this.hasP2pApplicationSubscription()) {
525
754
  this.peerCatalogDirty = false;
526
- const result = await this.peers({}, true);
755
+ const result = await this.runAutomaticPull('message.peers', () => this.peers({}, true));
756
+ if (!this.hasP2pApplicationSubscription()) {
757
+ this.peerCatalogDirty = true;
758
+ return;
759
+ }
527
760
  const rows = [...result.peers];
528
761
  const hint = (row) => this.peerPullPriority(this.peerNamespace(String(row.sender_aid)));
529
762
  rows.sort((left, right) => hint(left) - hint(right));
@@ -543,9 +776,29 @@ export class MessageDeliveryEngine {
543
776
  }
544
777
  schedulePeerRecovery(sender, head = 0, volatile = false) {
545
778
  const client = this.runtime.client;
546
- if (client._sessionOptions?.background_sync === false || !sender || client._closing)
779
+ if (!sender || client._closing)
547
780
  return;
548
781
  this.ensurePeerContext();
782
+ if (Number.isSafeInteger(head) && head > 0 && !volatile) {
783
+ const namespace = this.peerNamespace(sender);
784
+ const committedAck = this.peerCommittedAck(namespace);
785
+ const serverAck = this.peerServerConfirmedAcks.get(namespace) ?? 0;
786
+ if (head <= committedAck && serverAck >= committedAck) {
787
+ const existing = this.peerRecoveryNeeds.get(sender);
788
+ if (!existing || this.peerRecoveryTasks.has(sender))
789
+ return;
790
+ if (Number.isSafeInteger(existing.head) && existing.head > 0 && !existing.volatile && existing.head <= committedAck) {
791
+ this.peerRecoveryNeeds.delete(sender);
792
+ return;
793
+ }
794
+ }
795
+ }
796
+ const namespace = this.peerNamespace(sender);
797
+ this.peerPushVersions.set(namespace, {});
798
+ if (!this.hasP2pApplicationSubscription() || client._sessionOptions?.background_sync === false) {
799
+ this.peerForwardNeeds.set(namespace, {});
800
+ return;
801
+ }
549
802
  const need = this.peerRecoveryNeeds.get(sender) ?? { head: 0, volatile: false, pending: false };
550
803
  need.head = Math.max(need.head, head);
551
804
  need.volatile ||= volatile;
@@ -555,15 +808,67 @@ export class MessageDeliveryEngine {
555
808
  return;
556
809
  const generation = this.captureInlineAckGeneration();
557
810
  const task = (async () => {
558
- await this.initializePeer(sender);
559
- const state = this.syncState(this.peerNamespace(sender));
560
- if (this.peerOwnType() === 'human' && need.head > state.head && state.ack >= state.tail - 1) {
561
- await client._v2E2EE.pullPeerV2(sender, { window_mode: 'tail', background: true, retainMessages: false, budget: { remaining: 1 } });
811
+ await this.runAutomaticPull(`peer-init:${sender}`, () => this.initializePeer(sender));
812
+ if (!this.hasP2pApplicationSubscription())
813
+ return;
814
+ const namespace = this.peerNamespace(sender);
815
+ if (this.shouldTailPeer(namespace, need.head, _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize)) {
816
+ const forwardNeed = {};
817
+ this.peerForwardNeeds.set(namespace, forwardNeed);
818
+ need.pending = false;
819
+ const pushVersion = this.peerPushVersions.get(namespace);
820
+ const catalogVersion = this.peerCatalogVersion;
821
+ const autoTail = { head: need.head, forwardNeed, forward: false };
822
+ const tail = await this.runAutomaticPull(namespace, async () => {
823
+ const beforeAck = this.peerCommittedAck(namespace);
824
+ const beforeVolatile = this.peerVolatilePullCursor(sender);
825
+ const result = await client._v2E2EE.pullPeerV2(sender, { window_mode: 'tail', background: true, retainMessages: false, budget: { remaining: 1 }, autoTail });
826
+ if (autoTail.forward && !result.complete && this.peerCommittedAck(namespace) <= beforeAck && this.peerVolatilePullCursor(sender) === beforeVolatile) {
827
+ throw new Error('peer recovery made no progress');
828
+ }
829
+ return result;
830
+ });
831
+ if (generation !== this.captureInlineAckGeneration() || this.peerRecoveryNeeds.get(sender) !== need)
832
+ return;
833
+ if (autoTail.forward) {
834
+ if (tail.complete && this.peerForwardNeeds.get(namespace) === forwardNeed
835
+ && this.peerPushVersions.get(namespace) === pushVersion && this.peerCatalogVersion === catalogVersion)
836
+ this.peerForwardNeeds.delete(namespace);
837
+ if (tail.complete && Number(tail.ack_seq) >= need.head && !need.pending
838
+ && this.peerPushVersions.get(namespace) === pushVersion && this.peerCatalogVersion === catalogVersion) {
839
+ this.peerRecoveryNeeds.delete(sender);
840
+ return;
841
+ }
842
+ await new Promise((resolve) => setTimeout(resolve, BACKGROUND_PULL_RETRY_DELAY_MS));
843
+ }
844
+ else if (this.canClosePeerTail(namespace, tail, pushVersion, catalogVersion, need)) {
845
+ this.peerRecoveryNeeds.delete(sender);
846
+ if (this.peerForwardNeeds.get(namespace) === forwardNeed)
847
+ this.peerForwardNeeds.delete(namespace);
848
+ return;
849
+ }
562
850
  }
563
- while (generation === this.captureInlineAckGeneration() && this.peerRecoveryNeeds.has(sender)) {
851
+ while (generation === this.captureInlineAckGeneration() && this.peerRecoveryNeeds.has(sender) && this.hasP2pApplicationSubscription()) {
564
852
  need.pending = false;
565
- const result = await client._v2E2EE.pullPeerV2(sender, { background: true, retainMessages: false, budget: { remaining: 100 } });
566
- if (result.complete && result.ack_seq >= need.head && !need.pending) {
853
+ const forwardNeed = this.peerForwardNeeds.get(namespace);
854
+ const pushVersion = this.peerPushVersions.get(namespace);
855
+ const catalogVersion = this.peerCatalogVersion;
856
+ const result = await this.runAutomaticPull(namespace, async () => {
857
+ const beforeAck = this.peerCommittedAck(namespace);
858
+ const beforeVolatile = this.peerVolatilePullCursor(sender);
859
+ const result = await client._v2E2EE.pullPeerV2(sender, { background: true, retainMessages: false, budget: { remaining: 100 } });
860
+ if (!result.complete && this.peerCommittedAck(namespace) <= beforeAck && this.peerVolatilePullCursor(sender) === beforeVolatile) {
861
+ throw new Error('peer recovery made no progress');
862
+ }
863
+ return result;
864
+ });
865
+ if (generation !== this.captureInlineAckGeneration() || this.peerRecoveryNeeds.get(sender) !== need)
866
+ return;
867
+ if (result.complete && this.peerForwardNeeds.get(namespace) === forwardNeed
868
+ && this.peerPushVersions.get(namespace) === pushVersion && this.peerCatalogVersion === catalogVersion)
869
+ this.peerForwardNeeds.delete(namespace);
870
+ if (result.complete && result.ack_seq >= need.head && !need.pending
871
+ && this.peerPushVersions.get(namespace) === pushVersion && this.peerCatalogVersion === catalogVersion) {
567
872
  this.peerRecoveryNeeds.delete(sender);
568
873
  break;
569
874
  }
@@ -979,6 +1284,7 @@ export class MessageDeliveryEngine {
979
1284
  return processed.map((message) => this.publicVolatileMessage(message));
980
1285
  }
981
1286
  resetInlineAckState() {
1287
+ this.resetAutomaticPullBackoff();
982
1288
  this.syncStopped('aborted');
983
1289
  this.deliveryChangedGeneration += 1;
984
1290
  this.pendingDeliveryChanges = null;
@@ -1003,8 +1309,12 @@ export class MessageDeliveryEngine {
1003
1309
  this.peerCatalogTask = null;
1004
1310
  this.peerCatalogDirty = false;
1005
1311
  this.peerRecoveryNeeds.clear();
1312
+ this.peerForwardNeeds.clear();
1313
+ this.peerPushVersions.clear();
1314
+ this.peerCatalogVersion = null;
1006
1315
  this.peerPullHints.clear();
1007
1316
  this.peerPullConfirmedThrough.clear();
1317
+ this.peerServerConfirmedAcks.clear();
1008
1318
  this.peerRecoveryTasks.clear();
1009
1319
  }
1010
1320
  deliveryChangedNamespace(event, payload) {
@@ -1295,6 +1605,8 @@ export class MessageDeliveryEngine {
1295
1605
  this.schedulePendingGroupPullIfNeeded(ns, 'pull-gate-idle');
1296
1606
  }
1297
1607
  recordBackgroundPullNeed(ns, throughSeq = 0, probe = false) {
1608
+ if (this.automaticGroupPullStopped(ns))
1609
+ return;
1298
1610
  const client = this.runtime.client;
1299
1611
  if (client._sessionOptions?.background_sync === false || !client._aid
1300
1612
  || !(ns === `p2p:${client._aid}` || ns.startsWith('group:') || ns.startsWith('group_event:')))
@@ -1329,6 +1641,8 @@ export class MessageDeliveryEngine {
1329
1641
  finishBackgroundPull(ns, generation, afterSeq, failed) {
1330
1642
  if (generation !== this.captureInlineAckGeneration())
1331
1643
  return;
1644
+ if (this.automaticGroupPullStopped(ns))
1645
+ return;
1332
1646
  const need = this.backgroundPullNeeds.get(ns);
1333
1647
  if (need) {
1334
1648
  if (failed)
@@ -1362,6 +1676,12 @@ export class MessageDeliveryEngine {
1362
1676
  }
1363
1677
  let retryAt = Infinity;
1364
1678
  for (const [ns, need] of this.backgroundPullNeeds) {
1679
+ if (this.automaticGroupPullStopped(ns)) {
1680
+ this.backgroundPullNeeds.delete(ns);
1681
+ continue;
1682
+ }
1683
+ if (ns.startsWith('p2p:') && !this.hasP2pApplicationSubscription())
1684
+ continue;
1365
1685
  if (this.backgroundPullTasks.has(ns) || this.realtimeSyncing?.has(ns) || this.onlineUnreadHintTasks?.has(ns))
1366
1686
  continue;
1367
1687
  const eventScope = ns.startsWith('group_event:');
@@ -1428,6 +1748,8 @@ export class MessageDeliveryEngine {
1428
1748
  client._safeAsync(this.fillP2pGap());
1429
1749
  }
1430
1750
  recordPendingGroupPull(ns, seq) {
1751
+ if (this.automaticGroupPullStopped(ns))
1752
+ return;
1431
1753
  if (!ns || !Number.isSafeInteger(seq) || seq <= 0)
1432
1754
  return;
1433
1755
  const pending = this.pendingGroupPullUpper ?? new Map();
@@ -1435,6 +1757,8 @@ export class MessageDeliveryEngine {
1435
1757
  pending.set(ns, Math.max(pending.get(ns) ?? 0, seq));
1436
1758
  }
1437
1759
  schedulePendingGroupPullIfNeeded(ns, reason) {
1760
+ if (this.automaticGroupPullStopped(ns))
1761
+ return false;
1438
1762
  const pending = this.pendingGroupPullUpper;
1439
1763
  if (!pending)
1440
1764
  return false;
@@ -2580,13 +2904,15 @@ export class MessageDeliveryEngine {
2580
2904
  };
2581
2905
  if (client._rpcPipeline) {
2582
2906
  const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 });
2583
- await client._rpcPipeline.runPullNamespaceSerialized(key, operation, true);
2907
+ await this.runAutomaticPull(ns, operation, (attempt) => client._rpcPipeline.runPullNamespaceSerialized(key, attempt, true));
2584
2908
  }
2585
2909
  else {
2586
- await client._withBackgroundRpc(operation);
2910
+ await this.runAutomaticPull(ns, operation, (attempt) => client._withBackgroundRpc(attempt));
2587
2911
  }
2588
2912
  }
2589
2913
  async fillP2pGap() {
2914
+ if (!this.hasP2pApplicationSubscription())
2915
+ return;
2590
2916
  const client = this.runtime.client;
2591
2917
  if (client._p2pPeerPullEnabled) {
2592
2918
  this.refreshPendingPeers();
@@ -2611,9 +2937,9 @@ export class MessageDeliveryEngine {
2611
2937
  // Gap Fill 本身就是 Forward Pull,必须沿用协调器的完整事务:
2612
2938
  // 原始页提交成功后由协调器负责 Piggyback/终端 ACK。
2613
2939
  const operation = () => client._pullV2(afterSeq, 50, { gateLocked: true });
2614
- const messages = client._rpcPipeline
2615
- ? await client._rpcPipeline.runPullSerialized(client._rpcPipeline.pullGateKeyForCall('message.pull', { after_seq: afterSeq, limit: 50 }), operation, true)
2616
- : await client._withBackgroundRpc(operation);
2940
+ const messages = await this.runAutomaticPull(ns, operation, (attempt) => client._rpcPipeline
2941
+ ? client._rpcPipeline.runPullSerialized(client._rpcPipeline.pullGateKeyForCall('message.pull', { after_seq: afterSeq, limit: 50 }), attempt, true)
2942
+ : client._withBackgroundRpc(attempt));
2617
2943
  filled = messages.length;
2618
2944
  this.prunePushedSeqs(ns);
2619
2945
  client._clientLog.debug(`P2P message gap fill done: after_seq=${afterSeq}, filled=${filled}`);
@@ -2638,6 +2964,8 @@ export class MessageDeliveryEngine {
2638
2964
  if (!groupId)
2639
2965
  return;
2640
2966
  const ns = `group:${groupId}`;
2967
+ if (this.automaticGroupPullStopped(ns))
2968
+ return;
2641
2969
  const afterSeq = client._seqTracker.getContiguousSeq(ns);
2642
2970
  const generation = this.captureInlineAckGeneration();
2643
2971
  const dedupKey = `group_msg:${groupId}:${afterSeq}`;
@@ -2650,9 +2978,9 @@ export class MessageDeliveryEngine {
2650
2978
  let failed = false;
2651
2979
  try {
2652
2980
  const operation = () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true, background: true });
2653
- const messages = client._rpcPipeline
2654
- ? await client._rpcPipeline.runPullNamespaceSerialized(client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 }), operation, true)
2655
- : await client._withBackgroundRpc(operation);
2981
+ const messages = await this.runAutomaticPull(ns, operation, (attempt) => client._rpcPipeline
2982
+ ? client._rpcPipeline.runPullNamespaceSerialized(client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 }), attempt, true)
2983
+ : client._withBackgroundRpc(attempt));
2656
2984
  filled = messages.length;
2657
2985
  this.prunePushedSeqs(ns);
2658
2986
  client._clientLog.debug(`group message gap fill done: group=${groupId}, after_seq=${afterSeq}, filled=${filled}`);
@@ -2708,6 +3036,8 @@ export class MessageDeliveryEngine {
2708
3036
  if (!groupId)
2709
3037
  return;
2710
3038
  const ns = `group_event:${groupId}`;
3039
+ if (this.automaticGroupPullStopped(ns))
3040
+ return;
2711
3041
  const generation = this.captureInlineAckGeneration();
2712
3042
  const requestedAfterSeq = Number(options.afterSeq ?? 0);
2713
3043
  const afterSeq = Number.isSafeInteger(requestedAfterSeq) && requestedAfterSeq > 0
@@ -2751,10 +3081,10 @@ export class MessageDeliveryEngine {
2751
3081
  let result;
2752
3082
  if (client._rpcPipeline) {
2753
3083
  const pullKey = client._rpcPipeline.pullGateKeyForCall('group.pull_events', request);
2754
- result = await client._rpcPipeline.runPullSerialized(pullKey, () => client.call('group.pull_events', request), true);
3084
+ result = await this.runAutomaticPull(ns, () => client.call('group.pull_events', request), (attempt) => client._rpcPipeline.runPullSerialized(pullKey, attempt, true));
2755
3085
  }
2756
3086
  else {
2757
- result = await client.call('group.pull_events', request);
3087
+ result = await this.runAutomaticPull(ns, () => client.call('group.pull_events', request));
2758
3088
  }
2759
3089
  if (!isJsonObject(result))
2760
3090
  return;
@@ -2923,6 +3253,8 @@ export class MessageDeliveryEngine {
2923
3253
  enqueueOnlineUnreadEventHint(data) {
2924
3254
  const client = this.runtime.client;
2925
3255
  const groupId = String(data.group_id ?? '').trim();
3256
+ if (this.automaticGroupPullStopped(`group_event:${groupId}`))
3257
+ return;
2926
3258
  if (!groupId || client._sessionOptions?.background_sync === false)
2927
3259
  return;
2928
3260
  this.recordBackgroundPullNeed(`group_event:${groupId}`, positiveSafeSequenceHint(data.event_seq), true);
@@ -3252,9 +3584,9 @@ export class MessageDeliveryEngine {
3252
3584
  return { messages, raw_count: messages.length };
3253
3585
  };
3254
3586
  if (!client._rpcPipeline)
3255
- return await invoke();
3587
+ return await this.runAutomaticPull(ns, invoke);
3256
3588
  const key = client._rpcPipeline.pullGateKeyForCall('message.pull', request);
3257
- return await client._rpcPipeline.runPullSerialized(key, invoke, background);
3589
+ return await this.runAutomaticPull(ns, invoke, (attempt) => client._rpcPipeline.runPullSerialized(key, attempt, background));
3258
3590
  };
3259
3591
  if (order.length === 1 && order[0] === 'tail') {
3260
3592
  result = await run({ window_mode: 'tail', after_seq: this.syncState(ns).ack, limit: pageLimit }, false);
@@ -3330,9 +3662,9 @@ export class MessageDeliveryEngine {
3330
3662
  return { messages, raw_count: messages.length };
3331
3663
  };
3332
3664
  if (!client._rpcPipeline)
3333
- return await invoke();
3665
+ return await this.runAutomaticPull(ns, invoke);
3334
3666
  const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, ...request });
3335
- return await client._rpcPipeline.runPullNamespaceSerialized(key, invoke, background);
3667
+ return await this.runAutomaticPull(ns, invoke, (attempt) => client._rpcPipeline.runPullNamespaceSerialized(key, attempt, background));
3336
3668
  };
3337
3669
  if (order.length === 1 && order[0] === 'tail') {
3338
3670
  result = await run({ window_mode: 'tail', after_seq: this.syncState(ns).ack, limit: pageLimit }, false);
@@ -3373,9 +3705,9 @@ export class MessageDeliveryEngine {
3373
3705
  return { messages, raw_count: messages.length };
3374
3706
  };
3375
3707
  if (!client._rpcPipeline)
3376
- return await invoke();
3708
+ return await this.runAutomaticPull(ns, invoke);
3377
3709
  const key = client._rpcPipeline.pullGateKeyForCall('message.pull', request);
3378
- return await client._rpcPipeline.runPullSerialized(key, invoke, background);
3710
+ return await this.runAutomaticPull(ns, invoke, (attempt) => client._rpcPipeline.runPullSerialized(key, attempt, background));
3379
3711
  }
3380
3712
  async runGroupForwardRecovery(groupId, ns, pageLimit, background = false, throughSeq = 0) {
3381
3713
  const client = this.runtime.client;
@@ -3387,9 +3719,9 @@ export class MessageDeliveryEngine {
3387
3719
  return { messages, raw_count: messages.length };
3388
3720
  };
3389
3721
  if (!client._rpcPipeline)
3390
- return await invoke();
3722
+ return await this.runAutomaticPull(ns, invoke);
3391
3723
  const key = client._rpcPipeline.pullGateKeyForCall('group.pull', request);
3392
- return await client._rpcPipeline.runPullNamespaceSerialized(key, invoke, background);
3724
+ return await this.runAutomaticPull(ns, invoke, (attempt) => client._rpcPipeline.runPullNamespaceSerialized(key, attempt, background));
3393
3725
  }
3394
3726
  syncState(ns) {
3395
3727
  const tracker = this.runtime.client._seqTracker;
@@ -4018,15 +4350,31 @@ export class MessageDeliveryEngine {
4018
4350
  return;
4019
4351
  }
4020
4352
  const sender = String(head.sender_aid);
4021
- const ns = await this.initializePeer(sender);
4353
+ this.ensurePeerContext();
4354
+ this.peerPushVersions.set(this.peerNamespace(sender), {});
4355
+ if (!hasNonEmptyInlineMessage(head.inline_message) && !this.isVolatileMessage(head) && !this.hasP2pApplicationSubscription())
4356
+ return;
4022
4357
  const generation = this.captureInlineAckGeneration();
4358
+ let ns;
4359
+ try {
4360
+ ns = await this.runAutomaticPull(`peer-init:${sender}`, () => this.initializePeer(sender));
4361
+ }
4362
+ catch (error) {
4363
+ if (generation === this.captureInlineAckGeneration())
4364
+ this.schedulePeerRecovery(sender, positiveSafeSequenceHint(head.seq) ?? 0, this.isVolatileMessage(head));
4365
+ throw error;
4366
+ }
4023
4367
  const seq = positiveSafeSequenceHint(head.seq);
4024
4368
  if (this.isVolatileMessage(head)) {
4025
4369
  await client._v2E2EE.publishV2VolatileMessages([head], 'push', false);
4026
4370
  return;
4027
4371
  }
4028
4372
  if (!seq) {
4029
- this.refreshPendingPeers();
4373
+ if (client._sessionOptions?.background_sync === false && this.hasP2pApplicationSubscription()) {
4374
+ await this.runAutomaticPull(ns, () => client._v2E2EE.pullPeerV2(sender, { limit: _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize, budget: { remaining: 1 } }));
4375
+ }
4376
+ else
4377
+ this.refreshPendingPeers();
4030
4378
  return;
4031
4379
  }
4032
4380
  client._seqTracker.updateMaxSeen(ns, seq);
@@ -4065,14 +4413,14 @@ export class MessageDeliveryEngine {
4065
4413
  if (consumed?.acquired && consumed.value)
4066
4414
  return;
4067
4415
  }
4068
- if (seq <= this.syncState(ns).ack)
4416
+ if (seq <= this.syncState(ns).ack || !this.hasP2pApplicationSubscription())
4069
4417
  return;
4070
4418
  if (client._sessionOptions?.background_sync !== false) {
4071
4419
  this.schedulePeerRecovery(sender, seq);
4072
4420
  }
4073
4421
  else {
4074
- await client._v2E2EE.pullPeerV2(sender, { limit: _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize, budget: { remaining: 1 },
4075
- ...(this.peerOwnType() === 'human' ? { window_mode: 'tail' } : {}) });
4422
+ await this.runAutomaticPull(ns, () => client._v2E2EE.pullPeerV2(sender, { limit: _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize, budget: { remaining: 1 },
4423
+ ...(this.shouldTailPeer(ns, seq, _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize) ? { window_mode: 'tail', autoTail: { head: seq, forward: false } } : {}) }));
4076
4424
  }
4077
4425
  }
4078
4426
  async onV2PushNotification(data, expectedVersion, inlineGeneration) {
@@ -4136,6 +4484,8 @@ export class MessageDeliveryEngine {
4136
4484
  this.retryPendingP2PInlineAck(ns, pushSeq, 'covered inline push');
4137
4485
  return;
4138
4486
  }
4487
+ if (!this.hasP2pApplicationSubscription())
4488
+ return;
4139
4489
  this.recordBackgroundPullNeed(ns, pushSeq);
4140
4490
  const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
4141
4491
  if (pullActive || !this.beginRealtimeSync(ns)) {
@@ -4167,6 +4517,8 @@ export class MessageDeliveryEngine {
4167
4517
  if (!groupId)
4168
4518
  return;
4169
4519
  const ns = `group:${groupId}`;
4520
+ if (this.automaticGroupPullStopped(ns))
4521
+ return;
4170
4522
  const generation = this.captureInlineAckGeneration();
4171
4523
  const afterSeq = this.syncState(ns).ack;
4172
4524
  this.recordBackgroundPullNeed(ns, positiveSafeSequenceHint(data.seq));
@@ -4240,6 +4592,8 @@ export class MessageDeliveryEngine {
4240
4592
  return;
4241
4593
  }
4242
4594
  const ns = `group:${groupId}`;
4595
+ if (this.automaticGroupPullStopped(ns))
4596
+ return;
4243
4597
  let inlineConsumed = false;
4244
4598
  const pendingInlineAck = this.pendingGroupInlineAcks?.get(ns);
4245
4599
  const inlineBlockedByPendingAck = Boolean(pendingInlineAck && pendingInlineAck.seq !== seq);
@@ -4649,6 +5003,7 @@ export class MessageDeliveryEngine {
4649
5003
  this.seqTrackerNamespaceGenerations.set(ns, (this.seqTrackerNamespaceGenerations.get(ns) ?? 0) + 1);
4650
5004
  this.committedSeqTrackerStates.delete(ns);
4651
5005
  this.initializedPeers.delete(ns);
5006
+ this.peerServerConfirmedAcks.delete(ns);
4652
5007
  }
4653
5008
  this.dropSeqTrackerPending(ns);
4654
5009
  const saveSeq = client._tokenStore.saveSeq;