@agentunion/fastaun 0.5.20 → 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 (128) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/_packed_docs/CHANGELOG.md +104 -0
  3. package/_packed_docs/INDEX.md +59 -34
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +59 -52
  5. package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/344/270/216/346/215/242owner/347/274/226/346/216/222/346/226/271/346/241/210.md +530 -0
  6. package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/345/256/214/346/225/264/350/256/276/350/256/241/344/270/216/345/256/236/346/226/275/350/256/241/345/210/222.md +685 -0
  7. package/_packed_docs/agent.md/working/agent.md-schema/344/270/216/347/255/276/345/217/221/351/252/214/347/255/276/346/265/201/347/250/213/346/276/204/346/270/205.md +349 -0
  8. package/_packed_docs/agent.md/working//346/234/254/345/234/260Docker/346/265/213/350/257/225Bug/346/270/205/345/215/225.md +40 -0
  9. 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
  10. 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
  11. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1433 -1392
  12. package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
  13. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +30 -8
  14. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +2 -0
  15. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +33 -22
  16. package/_packed_docs/sdk/0.5.21-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +368 -0
  17. package/_packed_docs/sdk/0.5.21-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +350 -0
  18. 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
  19. 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
  20. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
  21. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
  22. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
  23. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +4 -1
  24. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +42 -39
  25. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +163 -14
  26. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +48 -47
  27. package/_packed_docs/sdk/09-group-rpc-manual.md +15 -2
  28. package/_packed_docs/sdk/09-message-rpc-manual.md +250 -160
  29. package/_packed_docs/sdk/09-storage-rpc-manual.md +64 -7
  30. package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +73 -0
  31. package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +90 -0
  32. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +33 -25
  33. package/_packed_docs/sdk/INDEX.md +67 -33
  34. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +74 -57
  35. 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 -300
  36. 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 -382
  37. 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 -708
  38. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +28 -20
  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 +190 -97
  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 +158 -127
  41. package/dist/agent-md-schema.js +6 -4
  42. package/dist/agent-md-schema.js.map +1 -1
  43. package/dist/agent-md.d.ts +22 -2
  44. package/dist/agent-md.js +92 -68
  45. package/dist/agent-md.js.map +1 -1
  46. package/dist/aid-store.d.ts +13 -0
  47. package/dist/aid-store.js +50 -15
  48. package/dist/aid-store.js.map +1 -1
  49. package/dist/auth.d.ts +1 -0
  50. package/dist/auth.js +22 -2
  51. package/dist/auth.js.map +1 -1
  52. package/dist/cert-utils.d.ts +1 -0
  53. package/dist/cert-utils.js +13 -0
  54. package/dist/cert-utils.js.map +1 -1
  55. package/dist/client/delivery.d.ts +87 -1
  56. package/dist/client/delivery.js +1387 -139
  57. package/dist/client/delivery.js.map +1 -1
  58. package/dist/client/group-state.js +3 -0
  59. package/dist/client/group-state.js.map +1 -1
  60. package/dist/client/rpc-pipeline.d.ts +4 -1
  61. package/dist/client/rpc-pipeline.js +149 -37
  62. package/dist/client/rpc-pipeline.js.map +1 -1
  63. package/dist/client/v2-e2ee.d.ts +12 -2
  64. package/dist/client/v2-e2ee.js +831 -304
  65. package/dist/client/v2-e2ee.js.map +1 -1
  66. package/dist/client.d.ts +26 -2
  67. package/dist/client.js +412 -68
  68. package/dist/client.js.map +1 -1
  69. package/dist/config.d.ts +4 -0
  70. package/dist/config.js +1 -0
  71. package/dist/config.js.map +1 -1
  72. package/dist/events.d.ts +18 -1
  73. package/dist/events.js +148 -9
  74. package/dist/events.js.map +1 -1
  75. package/dist/facades.d.ts +43 -1
  76. package/dist/facades.js +363 -7
  77. package/dist/facades.js.map +1 -1
  78. package/dist/group-fs.d.ts +1 -0
  79. package/dist/group-fs.js +37 -19
  80. package/dist/group-fs.js.map +1 -1
  81. package/dist/index.d.ts +5 -3
  82. package/dist/index.js +1 -1
  83. package/dist/index.js.map +1 -1
  84. package/dist/keystore/aid-db.d.ts +6 -1
  85. package/dist/keystore/aid-db.js +184 -42
  86. package/dist/keystore/aid-db.js.map +1 -1
  87. package/dist/keystore/index.d.ts +14 -0
  88. package/dist/keystore/local-token-store.d.ts +9 -1
  89. package/dist/keystore/local-token-store.js +79 -14
  90. package/dist/keystore/local-token-store.js.map +1 -1
  91. package/dist/org-protocol.d.ts +46 -0
  92. package/dist/org-protocol.js +802 -12
  93. package/dist/org-protocol.js.map +1 -1
  94. package/dist/ownership-v21.d.ts +36 -5
  95. package/dist/ownership-v21.js +168 -52
  96. package/dist/ownership-v21.js.map +1 -1
  97. package/dist/ownership-workflow.d.ts +24 -2
  98. package/dist/ownership-workflow.js +682 -41
  99. package/dist/ownership-workflow.js.map +1 -1
  100. package/dist/secret-store/file-store.js +84 -0
  101. package/dist/secret-store/file-store.js.map +1 -1
  102. package/dist/seq-tracker.d.ts +1 -0
  103. package/dist/seq-tracker.js +2 -2
  104. package/dist/seq-tracker.js.map +1 -1
  105. package/dist/storage/lowlevel.d.ts +6 -0
  106. package/dist/storage/lowlevel.js +24 -7
  107. package/dist/storage/lowlevel.js.map +1 -1
  108. package/dist/storage/read-cache.d.ts +70 -0
  109. package/dist/storage/read-cache.js +625 -0
  110. package/dist/storage/read-cache.js.map +1 -0
  111. package/dist/storage/vfs.d.ts +2 -0
  112. package/dist/storage/vfs.js +50 -15
  113. package/dist/storage/vfs.js.map +1 -1
  114. package/dist/tools/cross-sdk-agent.d.ts +86 -3
  115. package/dist/tools/cross-sdk-agent.js +758 -46
  116. package/dist/tools/cross-sdk-agent.js.map +1 -1
  117. package/dist/transport.d.ts +3 -0
  118. package/dist/transport.js +104 -46
  119. package/dist/transport.js.map +1 -1
  120. package/dist/types.d.ts +1 -0
  121. package/dist/v2/e2ee/encrypt-group.js +1 -0
  122. package/dist/v2/e2ee/encrypt-group.js.map +1 -1
  123. package/dist/v2/e2ee/encrypt-p2p.js +1 -0
  124. package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
  125. package/dist/v2/e2ee/types.d.ts +1 -0
  126. package/dist/version.d.ts +1 -1
  127. package/dist/version.js +1 -1
  128. package/package.json +1 -1
@@ -1,5 +1,8 @@
1
- import { slotIsolationKey } from '../config.js';
1
+ import { _INTERNAL_MESSAGE_FLOW_LIMITS, slotIsolationKey } from '../config.js';
2
+ import { parseFileFrontmatter } from '../agent-md-schema.js';
3
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
4
  import { trafficLogContext, withLogContext } from '../logger.js';
5
+ import { normalizeGroupAid } from '../group-id.js';
3
6
  import { ConnectionState, isJsonObject } from '../types.js';
4
7
  import { validateGroupAIDFormat } from '../validators.js';
5
8
  import { hasGroupMentionModeField, normalizeGroupMentionMode } from './mention-mode.js';
@@ -10,16 +13,35 @@ const GROUP_RECALL_SEEN_LIMIT = 10_000;
10
13
  const VOLATILE_SEEN_LIMIT = 10_000;
11
14
  const SEQ_TRACKER_PERSIST_FLUSH_DELAY_MS = 200;
12
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
+ }
13
34
  const DELIVERY_CHANGED_MESSAGE_EVENTS = new Set([
14
35
  'message.received',
15
36
  'group.message_created',
16
37
  ]);
38
+ const APPLICATION_DELIVERY_EVENTS = new Set(['message.received', 'message.recalled', 'group.message_created', 'group.message_recalled', 'group.changed']);
17
39
  const MAX_INLINE_PENDING_ACK_NAMESPACES = 4096;
18
40
  const APP_MESSAGE_ENVELOPE_KEYS = [
19
41
  'module_id', 'message_type', 'type', 'kind', 'version',
20
42
  'from', 'from_aid', 'sender_aid', 'to', 'to_aid', 'group_id',
21
43
  'timestamp', 'created_at', 'encrypted',
22
- 'context', 'protected_headers', 'headers', 'payload_type', 'mention_mode',
44
+ 'context', 'protected_headers', 'headers', 'payload_type', 'mention_mode', 'need_render',
23
45
  ];
24
46
  const RECALL_PAYLOAD_KEYS = [
25
47
  'message_ids', 'target_message_seqs',
@@ -100,9 +122,774 @@ export class MessageDeliveryEngine {
100
122
  volatileInFlightMessages = new Set();
101
123
  // 同一实例的 tracker 写入必须串行,避免旧的延迟 flush 覆盖新的 Forward 提交。
102
124
  seqTrackerWriteChain = Promise.resolve();
125
+ peerCatalogTask = null;
126
+ peerCatalogDirty = false;
127
+ peerCatalogQueries = new Map();
128
+ peerContext = '';
129
+ peerTracker = null;
130
+ peerContextGeneration = 0;
131
+ peerBootstrap = null;
132
+ initializedPeers = new Set();
133
+ committedSeqTrackerStates = new Map();
134
+ seqTrackerNamespaceGenerations = new Map();
135
+ peerRecoveryTasks = new Map();
136
+ peerRecoveryNeeds = new Map();
137
+ peerForwardNeeds = new Map();
138
+ peerPushVersions = new Map();
139
+ peerCatalogVersion = null;
140
+ peerPullHints = new Map();
141
+ peerPullConfirmedThrough = new Map();
142
+ peerServerConfirmedAcks = new Map();
143
+ peerVolatileCursors = new Map();
144
+ settledDeliveries = new Map();
145
+ deliveryInflight = new Map();
146
+ applicationPage = new AsyncLocalStorage();
147
+ applicationClaims = new Map();
148
+ volatileCompletions = new Map();
149
+ peerPageChains = new Map();
150
+ automaticPullFailures = new Map();
151
+ terminalGroupPulls = new Map();
152
+ automaticPullOperations = new WeakSet();
153
+ automaticPullContext = new AsyncLocalStorage();
154
+ automaticPullAbort = new AbortController();
103
155
  constructor(runtime) {
104
156
  this.runtime = runtime;
105
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
+ }
244
+ currentApplicationPage() {
245
+ const page = this.applicationPage.getStore();
246
+ return page?.collecting && page.batching ? page : undefined;
247
+ }
248
+ isApplicationPageCurrent(page) {
249
+ return page.context === this.ensurePeerContext() && page.generation === this.captureInlineAckGeneration()
250
+ && this.isSeqNamespaceGenerationCurrent(page.namespace, page.namespaceGeneration)
251
+ && (!this.isPullScopedSource(page.source) || this.isPullOperationCurrent(page.namespace));
252
+ }
253
+ ensureApplicationPageCurrent(page) {
254
+ if (!this.isApplicationPageCurrent(page))
255
+ throw new Error(this.isPullScopedSource(page.source) ? 'pull invalidated' : 'delivery invalidated');
256
+ }
257
+ snapshotApplicationHandlers() {
258
+ return this.runtime.client._dispatcher._snapshotApplicationHandlers?.(['message.batch', 'group.message_batch', 'group.event_batch']);
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
+ }
264
+ async runApplicationPage(namespace, source, operation, reservation, snapshot) {
265
+ const dispatcher = this.runtime.client._dispatcher;
266
+ if (typeof dispatcher._snapshotApplicationHandlers !== 'function')
267
+ return await operation();
268
+ const previous = this.applicationPage.getStore();
269
+ if (previous?.collecting && previous.namespace === namespace)
270
+ return await operation();
271
+ let resolve, reject;
272
+ const completion = new Promise((done, fail) => { resolve = done; reject = fail; });
273
+ void completion.catch(() => { });
274
+ const handlers = snapshot ?? this.snapshotApplicationHandlers(), batchEvent = namespace.startsWith('group_event:') ? 'group.event_batch'
275
+ : namespace.startsWith('group:') ? 'group.message_batch' : 'message.batch';
276
+ const page = { namespace, source, context: this.ensurePeerContext(), generation: this.captureInlineAckGeneration(),
277
+ namespaceGeneration: this.captureSeqNamespaceGeneration(namespace),
278
+ collecting: true, batching: (handlers.get(batchEvent)?.length ?? 0) > 0, handlers, reservation,
279
+ items: [], commits: [], afterDelivery: [], cleanup: [], claims: new Set(), bindings: new Map(), volatileSeen: new Set(), consumed: new Set(),
280
+ completion, resolve, reject };
281
+ return await this.applicationPage.run(page, async () => {
282
+ try {
283
+ const result = await operation();
284
+ page.collecting = false;
285
+ this.ensureApplicationPageCurrent(page);
286
+ if (page.items.length > 0) {
287
+ const batchHandlers = page.handlers.get(batchEvent) ?? [];
288
+ await dispatcher._publishAndWait(batchEvent, { namespace, items: page.items, source }, batchHandlers, reservation, () => this.isApplicationPageCurrent(page));
289
+ }
290
+ this.ensureApplicationPageCurrent(page);
291
+ for (const commit of page.commits)
292
+ commit();
293
+ for (const action of page.afterDelivery) {
294
+ await action();
295
+ this.ensureApplicationPageCurrent(page);
296
+ }
297
+ for (const cleanup of page.cleanup)
298
+ cleanup();
299
+ page.resolve();
300
+ return result;
301
+ }
302
+ catch (error) {
303
+ page.reject(error);
304
+ throw error;
305
+ }
306
+ finally {
307
+ page.collecting = false;
308
+ for (const key of page.claims)
309
+ if (this.applicationClaims.get(key) === page)
310
+ this.applicationClaims.delete(key);
311
+ }
312
+ });
313
+ }
314
+ completeApplicationDelivery(operation) {
315
+ const page = this.currentApplicationPage();
316
+ if (page)
317
+ page.commits.push(operation);
318
+ else
319
+ operation();
320
+ }
321
+ applicationNamespace(event, payload) {
322
+ const value = payload && typeof payload === 'object' ? payload : {};
323
+ if (event === 'group.changed')
324
+ return `group_event:${String(value.group_id ?? value.group_aid ?? '')}`;
325
+ if (event.startsWith('group.'))
326
+ return `group:${String(value.group_id ?? value.group_aid ?? '')}`;
327
+ const sender = String(value.sender_aid ?? value.from_aid ?? value.from ?? '');
328
+ return this.runtime.client._p2pPeerPullEnabled && sender ? this.peerNamespace(sender) : `p2p:${this.runtime.client._aid ?? ''}`;
329
+ }
330
+ applicationClaimKey(event, payload, namespace, context) {
331
+ const value = payload && typeof payload === 'object' ? payload : {};
332
+ const seq = Number(event === 'group.changed' ? value.event_seq : value.seq);
333
+ const identity = event === 'message.recalled' ? this.messageRecallDedupKey(value)
334
+ : event === 'group.message_recalled' ? this.groupRecallDedupKey(String(value.group_id ?? ''), value)
335
+ : Number.isSafeInteger(seq) && seq > 0 ? seq : value.volatile_cursor ?? value.message_id;
336
+ return identity === undefined ? '' : JSON.stringify([context, event, event.startsWith('message.') ? 'p2p' : namespace,
337
+ value.storage === 'volatile' ? String(value.sender_aid ?? value.from_aid ?? value.from ?? '') : '', identity]);
338
+ }
339
+ async waitForApplicationOwner(owner) {
340
+ if (this.runtime.client._dispatcher._isApplicationHandler?.())
341
+ throw new Error('delivery cannot wait inside an application handler');
342
+ await owner.completion;
343
+ }
344
+ async collectApplicationEvent(page, event, payload, completed) {
345
+ this.ensureApplicationPageCurrent(page);
346
+ const key = this.applicationClaimKey(event, payload, page.namespace, page.context);
347
+ if (key) {
348
+ if (page.claims.has(key))
349
+ return;
350
+ const owner = this.applicationClaims.get(key);
351
+ if (owner) {
352
+ await this.waitForApplicationOwner(owner);
353
+ this.ensureApplicationPageCurrent(page);
354
+ return;
355
+ }
356
+ this.applicationClaims.set(key, page);
357
+ page.claims.add(key);
358
+ }
359
+ page.items.push({ event, payload });
360
+ page.commits.push(completed);
361
+ }
362
+ peerNamespace(senderAid) {
363
+ if (!senderAid.trim())
364
+ throw new Error('sender_aid is required');
365
+ return `p2p.peer.v1/${Buffer.from(senderAid, 'utf8').toString('base64url')}`;
366
+ }
367
+ async runPeerPageSerialized(namespace, operation) {
368
+ const previous = this.peerPageChains.get(namespace) ?? Promise.resolve();
369
+ const current = previous.catch(() => undefined).then(operation);
370
+ this.peerPageChains.set(namespace, current);
371
+ try {
372
+ return await current;
373
+ }
374
+ finally {
375
+ if (this.peerPageChains.get(namespace) === current)
376
+ this.peerPageChains.delete(namespace);
377
+ if (this.peerCatalogDirty && this.peerPageChains.size === 0 && this.peerRecoveryTasks.size === 0)
378
+ this.refreshPendingPeers();
379
+ }
380
+ }
381
+ hasPeerPageActivity(namespace) {
382
+ return this.peerPageChains.has(namespace);
383
+ }
384
+ peerAckChanged() {
385
+ this.requestBackgroundPullRecovery();
386
+ }
387
+ peerPullPriority(namespace) {
388
+ this.ensurePeerContext();
389
+ return this.peerPullHints.get(namespace) ?? Math.max(this.peerPullConfirmedThrough.get(namespace) ?? 0, this.syncState(namespace).ack);
390
+ }
391
+ consumePeerPullHint(namespace, coveredThroughSeq, nextPendingSeq) {
392
+ const hint = this.peerPullPriority(namespace);
393
+ if (coveredThroughSeq < (this.peerPullConfirmedThrough.get(namespace) ?? 0))
394
+ return;
395
+ this.peerPullConfirmedThrough.set(namespace, coveredThroughSeq);
396
+ if (hint !== null && hint <= coveredThroughSeq)
397
+ this.peerPullHints.delete(namespace);
398
+ if (nextPendingSeq !== undefined && nextPendingSeq > coveredThroughSeq) {
399
+ this.peerPullHints.set(namespace, Math.max(this.peerPullHints.get(namespace) ?? 0, nextPendingSeq));
400
+ }
401
+ }
402
+ peerSender(namespace) {
403
+ return namespace.startsWith('p2p.peer.v1/') ? Buffer.from(namespace.slice(12), 'base64url').toString('utf8') : '';
404
+ }
405
+ ensurePeerContext() {
406
+ const client = this.runtime.client;
407
+ const context = JSON.stringify([client._aid ?? '', client._deviceId ?? '', slotIsolationKey(client._slotId)]);
408
+ if (context !== this.peerContext || client._seqTracker !== this.peerTracker) {
409
+ this.resetAutomaticPullBackoff();
410
+ this.peerContext = context;
411
+ this.peerTracker = client._seqTracker;
412
+ this.peerContextGeneration += 1;
413
+ this.peerBootstrap = null;
414
+ this.initializedPeers.clear();
415
+ this.committedSeqTrackerStates.clear();
416
+ this.seqTrackerNamespaceGenerations.clear();
417
+ this.peerVolatileCursors.clear();
418
+ this.peerRecoveryNeeds.clear();
419
+ this.peerForwardNeeds.clear();
420
+ this.peerPushVersions.clear();
421
+ this.peerCatalogVersion = null;
422
+ this.peerPullHints.clear();
423
+ this.peerPullConfirmedThrough.clear();
424
+ this.peerServerConfirmedAcks.clear();
425
+ this.peerRecoveryTasks.clear();
426
+ this.settledDeliveries.clear();
427
+ this.deliveryInflight.clear();
428
+ this.peerCatalogTask = null;
429
+ this.peerCatalogDirty = false;
430
+ this.peerCatalogQueries.clear();
431
+ }
432
+ return `${context}/${this.peerContextGeneration}`;
433
+ }
434
+ peerOwnType() {
435
+ const client = this.runtime.client;
436
+ try {
437
+ const current = client._currentAid;
438
+ if (!current?.isCertValid() || current.aid.toLowerCase() !== String(client._aid ?? '').toLowerCase())
439
+ return '';
440
+ const content = client._agentMdManager?.readContent?.(client._aid);
441
+ if (typeof content !== 'string' || !content)
442
+ return '';
443
+ const verified = current.verifyAgentMd(content);
444
+ if (!verified.ok || verified.data.status !== 'verified')
445
+ return '';
446
+ const type = parseFileFrontmatter(verified.data.payload).type;
447
+ return typeof type === 'string' ? type.trim() : '';
448
+ }
449
+ catch {
450
+ return '';
451
+ }
452
+ }
453
+ async initializePeer(senderAid, initialAfterSeq) {
454
+ const client = this.runtime.client;
455
+ const context = this.ensurePeerContext();
456
+ const ns = this.peerNamespace(senderAid);
457
+ if (this.initializedPeers.has(ns)) {
458
+ if (client._seqTracker.snapshotNamespace(ns) !== null)
459
+ return ns;
460
+ this.initializedPeers.delete(ns);
461
+ this.committedSeqTrackerStates.delete(ns);
462
+ }
463
+ const hadState = client._seqTracker.snapshotNamespace(ns) !== null;
464
+ if (this.peerBootstrap === null)
465
+ await this.peers({}, true);
466
+ if (context !== this.ensurePeerContext())
467
+ throw new Error('peer initialization invalidated');
468
+ if (this.initializedPeers.has(ns))
469
+ return ns;
470
+ if (!hadState) {
471
+ if (initialAfterSeq === undefined) {
472
+ let pageToken;
473
+ do {
474
+ const catalog = await this.peers({ filter: 'all', ...(pageToken ? { page_token: pageToken } : {}) }, true);
475
+ if (context !== this.ensurePeerContext())
476
+ throw new Error('peer initialization invalidated');
477
+ if (this.initializedPeers.has(ns))
478
+ return ns;
479
+ pageToken = catalog.has_more === true ? String(catalog.next_page_token ?? '') : undefined;
480
+ if (catalog.has_more === true && !pageToken)
481
+ throw new Error('all peer catalog is incomplete');
482
+ } while (pageToken);
483
+ }
484
+ const initial = initialAfterSeq ?? this.peerBootstrap ?? 0;
485
+ if (!Number.isSafeInteger(initial) || initial < 0)
486
+ throw new Error('invalid peer initialization baseline');
487
+ client._seqTracker.forceContiguousSeq(ns, initial);
488
+ }
489
+ const state = { ...client._seqTracker.getSyncState(ns) };
490
+ await this.writeSeqTrackerState(client._aid, client._deviceId, client._slotId, { [ns]: state }, true);
491
+ if (context !== this.ensurePeerContext())
492
+ throw new Error('peer initialization invalidated');
493
+ this.committedSeqTrackerStates.set(ns, state);
494
+ this.initializedPeers.add(ns);
495
+ return ns;
496
+ }
497
+ peerCommittedAck(namespace) {
498
+ this.ensurePeerContext();
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));
507
+ }
508
+ rememberCommittedSeqState(namespace) {
509
+ this.ensurePeerContext();
510
+ if (this.isReliableSeqNamespace(namespace) && !this.committedSeqTrackerStates.has(namespace)) {
511
+ this.committedSeqTrackerStates.set(namespace, { ...this.syncState(namespace) });
512
+ }
513
+ }
514
+ isReliableSeqNamespace(namespace) {
515
+ return namespace.startsWith('p2p.peer.v1/') || namespace.startsWith('group:') || namespace.startsWith('group_event:');
516
+ }
517
+ captureSeqNamespaceGeneration(namespace) {
518
+ return JSON.stringify([this.ensurePeerContext(), this.seqTrackerNamespaceGenerations.get(namespace) ?? 0]);
519
+ }
520
+ isSeqNamespaceGenerationCurrent(namespace, generation) {
521
+ return this.captureSeqNamespaceGeneration(namespace) === generation;
522
+ }
523
+ async peers(params = {}, background = false) {
524
+ const context = this.ensurePeerContext();
525
+ const key = JSON.stringify([context, params.filter ?? 'pending', params.limit ?? null, params.page_token ?? null]);
526
+ const existing = this.peerCatalogQueries.get(key);
527
+ if (existing) {
528
+ existing.application ||= !background;
529
+ return await existing.promise;
530
+ }
531
+ const query = { application: !background, promise: Promise.resolve({}) };
532
+ query.promise = this.queryPeers(params, () => !query.application);
533
+ this.peerCatalogQueries.set(key, query);
534
+ try {
535
+ return await query.promise;
536
+ }
537
+ finally {
538
+ if (this.peerCatalogQueries.get(key) === query)
539
+ this.peerCatalogQueries.delete(key);
540
+ }
541
+ }
542
+ async queryPeers(params, isBackground) {
543
+ const client = this.runtime.client;
544
+ const context = this.ensurePeerContext();
545
+ const generation = this.captureInlineAckGeneration();
546
+ const aid = client._aid;
547
+ const deviceId = client._deviceId;
548
+ const slotId = client._slotId;
549
+ const filter = String(params.filter ?? 'pending');
550
+ if (filter !== 'pending' && filter !== 'all')
551
+ throw new Error('invalid peers filter');
552
+ if (filter === 'pending' && ('limit' in params || 'page_token' in params))
553
+ throw new Error('pending peers cannot be paginated');
554
+ const raw = await client._rpcPipeline.rawCall('message.peers', {
555
+ ...params, filter, device_id: deviceId, slot_id: slotId,
556
+ }, { background: isBackground() });
557
+ if (context !== this.ensurePeerContext() || generation !== this.captureInlineAckGeneration())
558
+ throw new Error('peer catalog invalidated');
559
+ if (!raw || raw.cursor_scope !== 'peer' || raw.catalog_ready !== true || !Array.isArray(raw.peers))
560
+ throw new Error('peer catalog is not ready');
561
+ if (filter === 'pending' && (raw.has_more === true || raw.next_page_token))
562
+ throw new Error('pending peer catalog is incomplete');
563
+ const baseline = nonNegativeSafeSequenceHint(raw.bootstrap_floor_seq);
564
+ if (baseline === null)
565
+ throw new Error('invalid peer bootstrap_floor_seq');
566
+ if (this.peerBootstrap !== null && this.peerBootstrap !== baseline)
567
+ throw new Error('peer bootstrap changed');
568
+ if (this.peerBootstrap === null) {
569
+ const store = client._tokenStore;
570
+ const existing = await store.loadInstanceState?.(aid, deviceId, slotId);
571
+ if (context !== this.ensurePeerContext() || generation !== this.captureInlineAckGeneration())
572
+ throw new Error('peer catalog invalidated');
573
+ const saved = existing?.p2p_peer_bootstrap_floor_seq;
574
+ if (saved !== undefined && saved !== baseline)
575
+ throw new Error('persisted peer bootstrap changed');
576
+ if (typeof store.updateInstanceState !== 'function')
577
+ throw new Error('peer bootstrap persistence is unavailable');
578
+ await store.updateInstanceState(aid, deviceId, slotId, (state) => ({ ...state, p2p_peer_bootstrap_floor_seq: baseline }));
579
+ if (context !== this.ensurePeerContext() || generation !== this.captureInlineAckGeneration())
580
+ throw new Error('peer catalog invalidated');
581
+ this.peerBootstrap = baseline;
582
+ }
583
+ const rows = [];
584
+ for (const entry of raw.peers) {
585
+ if (!entry || typeof entry.sender_aid !== 'string' || !entry.sender_aid.trim())
586
+ throw new Error('peer catalog sender is invalid');
587
+ await this.initializePeer(entry.sender_aid, nonNegativeSafeSequenceHint(entry.initial_after_seq) ?? baseline);
588
+ if (context !== this.ensurePeerContext() || generation !== this.captureInlineAckGeneration())
589
+ throw new Error('peer catalog invalidated');
590
+ rows.push({ ...entry });
591
+ }
592
+ for (const row of rows) {
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);
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);
604
+ const confirmed = Math.max(this.peerPullConfirmedThrough.get(ns) ?? 0, nonNegativeSafeSequenceHint(row.server_ack_seq) ?? nonNegativeSafeSequenceHint(row.initial_after_seq) ?? 0);
605
+ this.peerPullConfirmedThrough.set(ns, confirmed);
606
+ const current = this.peerPullHints.get(ns);
607
+ if (current !== undefined && current <= confirmed)
608
+ this.peerPullHints.delete(ns);
609
+ if (hint !== null && hint >= confirmed)
610
+ this.peerPullHints.set(ns, Math.max(this.peerPullHints.get(ns) ?? 0, hint));
611
+ }
612
+ const snapshot = {
613
+ ...raw, filter, peers: rows, aid, device_id: deviceId, slot_id: slotId,
614
+ trigger: isBackground() ? 'background' : 'application', peer_count: rows.length,
615
+ };
616
+ if (!isBackground() || rows.length > 0)
617
+ client._dispatcher.enqueue('message.peers_snapshot', snapshot, () => context === this.ensurePeerContext() && generation === this.captureInlineAckGeneration());
618
+ return snapshot;
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
+ }
649
+ async pullPeers(params = {}, background = false) {
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();
654
+ const sender = String(params.sender_aid ?? '').trim();
655
+ if (!sender && ('after_seq' in params || 'after_volatile_cursor' in params || 'window_mode' in params || params.force === true))
656
+ throw new Error('explicit peer is required for after_seq, after_volatile_cursor, window_mode or force');
657
+ const snapshot = sender ? null : await this.peers({}, background);
658
+ const rows = sender ? [{ sender_aid: sender }] : snapshot.peers;
659
+ const budget = { remaining: Math.max(1, Math.trunc(Number(params.max_pages ?? 100) || 100)) };
660
+ const results = await Promise.all(rows.map(async (row) => {
661
+ const peer = String(row.sender_aid);
662
+ const peerPullVersion = this.capturePeerTailVersion(peer);
663
+ const ns = await this.initializePeer(peer);
664
+ try {
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
+ }
690
+ if (!budget.remaining) {
691
+ this.schedulePeerRecovery(peer, Number(tail.head_seq ?? 0), true);
692
+ return { ...tail, complete: false };
693
+ }
694
+ forwardNeed = this.peerForwardNeeds.get(ns);
695
+ const forwardPushVersion = this.peerPushVersions.get(ns);
696
+ const forwardCatalogVersion = this.peerCatalogVersion;
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);
705
+ const messages = [...new Map([...tail.messages, ...forward.messages].map((message) => [message.seq ?? message.volatile_cursor, message])).values()];
706
+ return { ...forward, messages, complete: forward.complete === true && !notified };
707
+ }
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;
730
+ }
731
+ catch (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) };
735
+ }
736
+ }));
737
+ return { cursor_scope: 'peer', peers: results, messages: results.flatMap((row) => row.messages), partial: results.some((row) => !row.complete) };
738
+ }
739
+ refreshPendingPeers() {
740
+ this.ensurePeerContext();
741
+ this.peerCatalogVersion = {};
742
+ if (!this.hasP2pApplicationSubscription())
743
+ return;
744
+ const client = this.runtime.client;
745
+ if (client._sessionOptions?.background_sync === false || !client._aid || client._closing || client.state !== ConnectionState.READY)
746
+ return;
747
+ this.ensurePeerContext();
748
+ this.peerCatalogDirty = true;
749
+ if (this.peerCatalogTask)
750
+ return;
751
+ const generation = this.captureInlineAckGeneration();
752
+ const task = (async () => {
753
+ while (this.peerCatalogDirty && generation === this.captureInlineAckGeneration() && this.hasP2pApplicationSubscription()) {
754
+ this.peerCatalogDirty = false;
755
+ const result = await this.runAutomaticPull('message.peers', () => this.peers({}, true));
756
+ if (!this.hasP2pApplicationSubscription()) {
757
+ this.peerCatalogDirty = true;
758
+ return;
759
+ }
760
+ const rows = [...result.peers];
761
+ const hint = (row) => this.peerPullPriority(this.peerNamespace(String(row.sender_aid)));
762
+ rows.sort((left, right) => hint(left) - hint(right));
763
+ for (const row of rows)
764
+ this.schedulePeerRecovery(String(row.sender_aid), Number(row.head_seq ?? 0), Boolean(row.volatile_has_more));
765
+ }
766
+ })();
767
+ this.peerCatalogTask = task.then(() => ({}), () => ({}));
768
+ client._safeAsync(task.catch(() => {
769
+ if (generation !== this.captureInlineAckGeneration())
770
+ return;
771
+ const timer = setTimeout(() => { if (generation === this.captureInlineAckGeneration())
772
+ this.refreshPendingPeers(); }, BACKGROUND_PULL_RETRY_DELAY_MS);
773
+ timer.unref?.();
774
+ }).finally(() => { if (generation === this.captureInlineAckGeneration())
775
+ this.peerCatalogTask = null; }));
776
+ }
777
+ schedulePeerRecovery(sender, head = 0, volatile = false) {
778
+ const client = this.runtime.client;
779
+ if (!sender || client._closing)
780
+ return;
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
+ }
802
+ const need = this.peerRecoveryNeeds.get(sender) ?? { head: 0, volatile: false, pending: false };
803
+ need.head = Math.max(need.head, head);
804
+ need.volatile ||= volatile;
805
+ need.pending = true;
806
+ this.peerRecoveryNeeds.set(sender, need);
807
+ if (this.peerRecoveryTasks.has(sender))
808
+ return;
809
+ const generation = this.captureInlineAckGeneration();
810
+ const task = (async () => {
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
+ }
850
+ }
851
+ while (generation === this.captureInlineAckGeneration() && this.peerRecoveryNeeds.has(sender) && this.hasP2pApplicationSubscription()) {
852
+ need.pending = false;
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) {
872
+ this.peerRecoveryNeeds.delete(sender);
873
+ break;
874
+ }
875
+ await new Promise((resolve) => setTimeout(resolve, BACKGROUND_PULL_RETRY_DELAY_MS));
876
+ }
877
+ })();
878
+ this.peerRecoveryTasks.set(sender, task);
879
+ client._safeAsync(task.catch(() => {
880
+ if (generation !== this.captureInlineAckGeneration())
881
+ return;
882
+ const timer = setTimeout(() => { if (generation === this.captureInlineAckGeneration())
883
+ this.schedulePeerRecovery(sender, need.head, need.volatile); }, BACKGROUND_PULL_RETRY_DELAY_MS);
884
+ timer.unref?.();
885
+ }).finally(() => {
886
+ if (generation === this.captureInlineAckGeneration()) {
887
+ this.peerRecoveryTasks.delete(sender);
888
+ if (this.peerCatalogDirty && this.peerRecoveryTasks.size === 0)
889
+ this.refreshPendingPeers();
890
+ }
891
+ }));
892
+ }
106
893
  isPullOperationCurrent(namespace = '') {
107
894
  const pipeline = this.runtime.client._rpcPipeline;
108
895
  return typeof pipeline?.isPullNamespaceCurrent === 'function'
@@ -170,15 +957,13 @@ export class MessageDeliveryEngine {
170
957
  const client = this.runtime.client;
171
958
  const response = result;
172
959
  const volatileMessages = method === 'message.pull' ? this.volatileMessages(response.messages) : [];
173
- if (volatileMessages.length > 0) {
174
- await this.publishPlainVolatileMessages(volatileMessages, 'pull', true);
175
- }
176
960
  this.ensurePullOperationCurrent(ns);
177
961
  this.syncStarted(ns);
178
962
  const messages = deduplicatePlainForwardPageMessages(response.messages, afterSeq);
179
963
  const coordinator = this.forwardCoordinator();
180
964
  const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
181
965
  const pageInlineGeneration = this.captureInlineAckGeneration();
966
+ const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
182
967
  const deferredServerCursor = Number(coordinator.pendingForwardCursor(ns, pageContigBefore) ?? 0);
183
968
  const deferredForwardAck = Number(coordinator.pendingForwardAck(ns, pageContigBefore) ?? 0);
184
969
  let pageTrackerSnapshot = null;
@@ -202,44 +987,51 @@ export class MessageDeliveryEngine {
202
987
  let publishedCount = 0;
203
988
  let committed = false;
204
989
  try {
205
- for (const rawMessage of messages) {
206
- this.ensurePullOperationCurrent(ns);
207
- const seq = positiveSafeSequenceHint(rawMessage.seq);
208
- if (method === 'message.pull') {
209
- const appEvent = this.p2pAppEventForMessage(rawMessage);
210
- if (await this.publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false)) {
211
- publishedCount += 1;
212
- }
990
+ await this.runApplicationPage(ns, 'pull', async () => {
991
+ if (volatileMessages.length > 0)
992
+ await this.publishPlainVolatileMessages(volatileMessages, 'pull', true);
993
+ for (const rawMessage of messages) {
213
994
  this.ensurePullOperationCurrent(ns);
214
- continue;
215
- }
216
- const message = normalizeGroupMentionMode(rawMessage);
217
- if (this.recallEventFromGroupMessage(message)) {
218
- if (await this.publishGroupRecallTombstone(groupId, seq, message, 'pull')) {
995
+ const seq = positiveSafeSequenceHint(rawMessage.seq);
996
+ if (method === 'message.pull') {
997
+ const appEvent = this.p2pAppEventForMessage(rawMessage);
998
+ if (await this.publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false)) {
999
+ publishedCount += 1;
1000
+ }
219
1001
  this.ensurePullOperationCurrent(ns);
1002
+ continue;
1003
+ }
1004
+ const message = normalizeGroupMentionMode(rawMessage);
1005
+ if (this.recallEventFromGroupMessage(message)) {
1006
+ if (await this.publishGroupRecallTombstone(groupId, seq, message, 'pull')) {
1007
+ this.ensurePullOperationCurrent(ns);
1008
+ this.markPublishedSeq(ns, seq);
1009
+ publishedCount += 1;
1010
+ }
1011
+ }
1012
+ else if (this.isSelfSentGroupMessage(message)) {
220
1013
  this.markPublishedSeq(ns, seq);
1014
+ }
1015
+ else if (await this.publishPulledMessage('group.message_created', ns, seq, message, false)) {
1016
+ this.ensurePullOperationCurrent(ns);
221
1017
  publishedCount += 1;
222
1018
  }
223
1019
  }
224
- else if (this.isSelfSentGroupMessage(message)) {
225
- this.markPublishedSeq(ns, seq);
226
- }
227
- else if (await this.publishPulledMessage('group.message_created', ns, seq, message, false)) {
228
- this.ensurePullOperationCurrent(ns);
229
- publishedCount += 1;
230
- }
231
- }
1020
+ this.ensurePullOperationCurrent(ns);
1021
+ if (pageInlineGeneration !== this.captureInlineAckGeneration())
1022
+ throw new Error('pull invalidated');
1023
+ const previousWindow = client._seqTracker.getSyncState(ns);
1024
+ const scanTarget = Math.max(pageContigBefore, ...messages.map((message) => positiveSafeSequenceHint(message.seq)), positiveSafeSequenceHint(response.contiguous_seq), retentionFloor, visibilityFloor, deferredServerCursor);
1025
+ const deliveryTarget = scanTarget >= previousWindow.tail - 1
1026
+ ? Math.max(scanTarget, previousWindow.head) : scanTarget;
1027
+ await this.drainOrderedMessages(ns, undefined, false, false, 'pull', undefined, deliveryTarget);
1028
+ });
232
1029
  this.ensurePullOperationCurrent(ns);
233
1030
  if (pageInlineGeneration !== this.captureInlineAckGeneration())
234
1031
  throw new Error('pull invalidated');
235
- const previousWindow = client._seqTracker.getSyncState(ns);
236
- const scanTarget = Math.max(pageContigBefore, ...messages.map((message) => positiveSafeSequenceHint(message.seq)), positiveSafeSequenceHint(response.contiguous_seq), retentionFloor, visibilityFloor, deferredServerCursor);
237
- const deliveryTarget = scanTarget >= previousWindow.tail - 1
238
- ? Math.max(scanTarget, previousWindow.head) : scanTarget;
239
- await this.drainOrderedMessages(ns, undefined, false, false, 'pull', undefined, deliveryTarget);
240
- this.ensurePullOperationCurrent(ns);
241
- if (pageInlineGeneration !== this.captureInlineAckGeneration())
1032
+ if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
242
1033
  throw new Error('pull invalidated');
1034
+ this.rememberCommittedSeqState(ns);
243
1035
  pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
244
1036
  trackerMutated = true;
245
1037
  client._seqTracker.onPullResult(ns, messages, afterSeq, Number.isSafeInteger(result.contiguous_seq)
@@ -255,11 +1047,14 @@ export class MessageDeliveryEngine {
255
1047
  this.ensurePullOperationCurrent(ns);
256
1048
  if (pageInlineGeneration !== this.captureInlineAckGeneration())
257
1049
  throw new Error('pull invalidated');
1050
+ if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
1051
+ throw new Error('pull invalidated');
258
1052
  }
259
1053
  committed = true;
260
1054
  }
261
1055
  catch (exc) {
262
1056
  if (trackerMutated && pageInlineGeneration === this.captureInlineAckGeneration() && this.isPullOperationCurrent(ns)
1057
+ && this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration)
263
1058
  && !committed && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
264
1059
  (client._seqTracker._restoreNamespaceSnapshotPreservingPush ?? client._seqTracker.restoreNamespaceSnapshot)
265
1060
  .call(client._seqTracker, ns, pageTrackerSnapshot);
@@ -330,23 +1125,39 @@ export class MessageDeliveryEngine {
330
1125
  this.volatileCursor = '';
331
1126
  this.volatileSeenMessages.clear();
332
1127
  this.volatileInFlightMessages.clear();
1128
+ for (const completion of this.volatileCompletions.values())
1129
+ completion.reject(new Error('delivery invalidated'));
1130
+ this.volatileCompletions.clear();
333
1131
  }
334
1132
  volatilePullCursor() {
335
1133
  this.ensureVolatileContext();
336
1134
  return this.volatileCursor;
337
1135
  }
1136
+ peerVolatilePullCursor(sender) {
1137
+ this.ensurePeerContext();
1138
+ return this.peerVolatileCursors.get(sender) ?? '';
1139
+ }
1140
+ setPeerVolatilePullCursor(sender, cursor) {
1141
+ this.ensurePeerContext();
1142
+ if (cursor)
1143
+ this.peerVolatileCursors.set(sender, cursor);
1144
+ else
1145
+ this.peerVolatileCursors.delete(sender);
1146
+ }
338
1147
  resetVolatilePullCursor() {
339
1148
  this.ensureVolatileContext();
340
1149
  this.volatileCursor = '';
341
1150
  }
342
1151
  volatileSeenKey(message) {
343
1152
  const messageId = String(message.message_id ?? '').trim();
344
- const sender = String(message.from_aid ?? message.from ?? '').trim();
1153
+ const sender = String(message.sender_aid ?? message.from_aid ?? message.from ?? '').trim();
345
1154
  const cursor = String(message.volatile_cursor ?? '').trim();
346
- return `${sender}|${messageId || cursor}`;
1155
+ return JSON.stringify([sender, messageId, cursor]);
347
1156
  }
348
1157
  isVolatileSeen(message) {
349
1158
  this.ensureVolatileContext();
1159
+ if (this.currentApplicationPage()?.volatileSeen.has(this.volatileSeenKey(message)))
1160
+ return true;
350
1161
  const now = Date.now();
351
1162
  for (const [key, expiresAt] of this.volatileSeenMessages) {
352
1163
  if (expiresAt <= now)
@@ -356,6 +1167,12 @@ export class MessageDeliveryEngine {
356
1167
  }
357
1168
  markVolatileSeen(message) {
358
1169
  this.ensureVolatileContext();
1170
+ const page = this.currentApplicationPage();
1171
+ if (page) {
1172
+ page.volatileSeen.add(this.volatileSeenKey(message));
1173
+ page.commits.push(() => this.markVolatileSeen(message));
1174
+ return;
1175
+ }
359
1176
  const now = Date.now();
360
1177
  const expiresAt = Math.max(now + 60_000, Number(message.expires_at ?? now + 1_800_000) || now + 1_800_000);
361
1178
  this.volatileSeenMessages.set(this.volatileSeenKey(message), expiresAt);
@@ -373,11 +1190,38 @@ export class MessageDeliveryEngine {
373
1190
  if (this.volatileInFlightMessages.has(key))
374
1191
  return 'inflight';
375
1192
  this.volatileInFlightMessages.add(key);
1193
+ let resolve, reject;
1194
+ const promise = new Promise((done, fail) => { resolve = done; reject = fail; });
1195
+ void promise.catch(() => { });
1196
+ const page = this.currentApplicationPage();
1197
+ const completion = { page, promise, resolve, reject };
1198
+ this.volatileCompletions.set(key, completion);
1199
+ if (page)
1200
+ void page.completion.then(resolve, reject).finally(() => {
1201
+ if (this.volatileCompletions.get(key) === completion) {
1202
+ this.volatileCompletions.delete(key);
1203
+ this.volatileInFlightMessages.delete(key);
1204
+ }
1205
+ });
376
1206
  return 'claimed';
377
1207
  }
1208
+ async waitVolatileDelivery(message) {
1209
+ const completion = this.volatileCompletions.get(this.volatileSeenKey(message));
1210
+ if (!completion || completion.page === this.currentApplicationPage())
1211
+ return;
1212
+ if (this.runtime.client._dispatcher._isApplicationHandler?.())
1213
+ throw new Error('delivery cannot wait inside an application handler');
1214
+ await completion.promise;
1215
+ }
378
1216
  finishVolatileDelivery(message) {
379
1217
  this.ensureVolatileContext();
380
- this.volatileInFlightMessages.delete(this.volatileSeenKey(message));
1218
+ const key = this.volatileSeenKey(message);
1219
+ const completion = this.volatileCompletions.get(key);
1220
+ if (completion?.page)
1221
+ return;
1222
+ completion?.resolve();
1223
+ this.volatileCompletions.delete(key);
1224
+ this.volatileInFlightMessages.delete(key);
381
1225
  }
382
1226
  publicVolatileMessage(message) {
383
1227
  const result = this.stripInternalSenderDeviceFields(message);
@@ -385,15 +1229,27 @@ export class MessageDeliveryEngine {
385
1229
  return result;
386
1230
  }
387
1231
  async ackVolatileMessages(messages, v2, advanceCursor) {
1232
+ const page = this.currentApplicationPage();
1233
+ if (page) {
1234
+ page.afterDelivery.push(() => this.ackVolatileMessages(messages, v2, advanceCursor));
1235
+ return undefined;
1236
+ }
388
1237
  const cursors = [...new Set(messages
389
1238
  .map((message) => String(message.volatile_cursor ?? '').trim())
390
1239
  .filter(Boolean))];
391
1240
  if (cursors.length === 0)
392
1241
  return undefined;
393
- const result = await this.runtime.client._callRawV2Rpc(v2 ? 'message.v2.ack' : 'message.ack', { volatile_cursors: cursors, _rpc_background: true });
1242
+ const client = this.runtime.client;
1243
+ const sender = client._p2pPeerPullEnabled ? String(messages[0]?.sender_aid ?? messages[0]?.from_aid ?? messages[0]?.from ?? '') : '';
1244
+ const result = sender
1245
+ ? await client._v2E2EE.ackPeerV2(sender, 0, cursors)
1246
+ : await client._callRawV2Rpc(v2 ? 'message.v2.ack' : 'message.ack', { volatile_cursors: cursors, _rpc_background: true });
394
1247
  if (advanceCursor) {
395
1248
  this.ensureVolatileContext();
396
- this.volatileCursor = cursors[cursors.length - 1] ?? '';
1249
+ if (sender)
1250
+ this.setPeerVolatilePullCursor(sender, cursors[cursors.length - 1] ?? '');
1251
+ else
1252
+ this.volatileCursor = cursors[cursors.length - 1] ?? '';
397
1253
  }
398
1254
  return result;
399
1255
  }
@@ -406,8 +1262,12 @@ export class MessageDeliveryEngine {
406
1262
  processed.push(rawMessage);
407
1263
  continue;
408
1264
  }
409
- if (claim === 'inflight')
1265
+ if (claim === 'inflight') {
1266
+ await this.waitVolatileDelivery(message);
1267
+ if (this.isVolatileSeen(message))
1268
+ processed.push(rawMessage);
410
1269
  continue;
1270
+ }
411
1271
  try {
412
1272
  const appEvent = this.p2pAppEventForMessage(message);
413
1273
  await this.runtime.client._publishAppEvent(appEvent.event, appEvent.payload, source);
@@ -424,6 +1284,7 @@ export class MessageDeliveryEngine {
424
1284
  return processed.map((message) => this.publicVolatileMessage(message));
425
1285
  }
426
1286
  resetInlineAckState() {
1287
+ this.resetAutomaticPullBackoff();
427
1288
  this.syncStopped('aborted');
428
1289
  this.deliveryChangedGeneration += 1;
429
1290
  this.pendingDeliveryChanges = null;
@@ -445,6 +1306,16 @@ export class MessageDeliveryEngine {
445
1306
  this.onlineUnreadHintTasks = null;
446
1307
  this.realtimeTailHeads = null;
447
1308
  this.syncSessions = null;
1309
+ this.peerCatalogTask = null;
1310
+ this.peerCatalogDirty = false;
1311
+ this.peerRecoveryNeeds.clear();
1312
+ this.peerForwardNeeds.clear();
1313
+ this.peerPushVersions.clear();
1314
+ this.peerCatalogVersion = null;
1315
+ this.peerPullHints.clear();
1316
+ this.peerPullConfirmedThrough.clear();
1317
+ this.peerServerConfirmedAcks.clear();
1318
+ this.peerRecoveryTasks.clear();
448
1319
  }
449
1320
  deliveryChangedNamespace(event, payload) {
450
1321
  if (event === 'group.message_created' && isJsonObject(payload)) {
@@ -716,7 +1587,8 @@ export class MessageDeliveryEngine {
716
1587
  return this.pendingGroupPullUpper?.get(ns) ?? 0;
717
1588
  }
718
1589
  hasAnyPendingPull() {
719
- return [...(this.pendingGroupPullUpper?.values() ?? [])].some((seq) => seq > 0)
1590
+ return this.peerCatalogDirty || this.peerRecoveryNeeds.size > 0 || this.peerRecoveryTasks.size > 0 || Boolean(this.peerCatalogTask)
1591
+ || [...(this.pendingGroupPullUpper?.values() ?? [])].some((seq) => seq > 0)
720
1592
  || this.backgroundPullNeeds.size > 0
721
1593
  || Boolean(this.onlineUnreadHintTasks?.size)
722
1594
  || Boolean(this.realtimeSyncing?.size)
@@ -733,6 +1605,8 @@ export class MessageDeliveryEngine {
733
1605
  this.schedulePendingGroupPullIfNeeded(ns, 'pull-gate-idle');
734
1606
  }
735
1607
  recordBackgroundPullNeed(ns, throughSeq = 0, probe = false) {
1608
+ if (this.automaticGroupPullStopped(ns))
1609
+ return;
736
1610
  const client = this.runtime.client;
737
1611
  if (client._sessionOptions?.background_sync === false || !client._aid
738
1612
  || !(ns === `p2p:${client._aid}` || ns.startsWith('group:') || ns.startsWith('group_event:')))
@@ -767,6 +1641,8 @@ export class MessageDeliveryEngine {
767
1641
  finishBackgroundPull(ns, generation, afterSeq, failed) {
768
1642
  if (generation !== this.captureInlineAckGeneration())
769
1643
  return;
1644
+ if (this.automaticGroupPullStopped(ns))
1645
+ return;
770
1646
  const need = this.backgroundPullNeeds.get(ns);
771
1647
  if (need) {
772
1648
  if (failed)
@@ -779,7 +1655,7 @@ export class MessageDeliveryEngine {
779
1655
  this.requestBackgroundPullRecovery();
780
1656
  }
781
1657
  requestBackgroundPullRecovery() {
782
- if (this.backgroundPullCheckQueued || this.backgroundPullNeeds.size === 0)
1658
+ if (this.backgroundPullCheckQueued || (this.backgroundPullNeeds.size === 0 && !this.peerCatalogDirty && this.peerRecoveryNeeds.size === 0))
783
1659
  return;
784
1660
  this.backgroundPullCheckQueued = true;
785
1661
  const generation = this.captureInlineAckGeneration();
@@ -790,8 +1666,22 @@ export class MessageDeliveryEngine {
790
1666
  const client = this.runtime.client;
791
1667
  if (client._sessionOptions?.background_sync === false || client.state !== ConnectionState.READY || client._closing)
792
1668
  return;
1669
+ if (client._p2pPeerPullEnabled) {
1670
+ if (this.peerCatalogDirty && !this.peerCatalogTask)
1671
+ this.refreshPendingPeers();
1672
+ for (const [sender, need] of this.peerRecoveryNeeds) {
1673
+ if (!this.peerRecoveryTasks.has(sender))
1674
+ this.schedulePeerRecovery(sender, need.head, need.volatile);
1675
+ }
1676
+ }
793
1677
  let retryAt = Infinity;
794
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;
795
1685
  if (this.backgroundPullTasks.has(ns) || this.realtimeSyncing?.has(ns) || this.onlineUnreadHintTasks?.has(ns))
796
1686
  continue;
797
1687
  const eventScope = ns.startsWith('group_event:');
@@ -845,6 +1735,8 @@ export class MessageDeliveryEngine {
845
1735
  }
846
1736
  onForegroundPullsDrained() {
847
1737
  const client = this.runtime.client;
1738
+ if (client._p2pPeerPullEnabled)
1739
+ return;
848
1740
  if (!client._aid || client._sessionOptions?.background_sync === false
849
1741
  || client.state !== ConnectionState.READY || client._closing)
850
1742
  return;
@@ -856,6 +1748,8 @@ export class MessageDeliveryEngine {
856
1748
  client._safeAsync(this.fillP2pGap());
857
1749
  }
858
1750
  recordPendingGroupPull(ns, seq) {
1751
+ if (this.automaticGroupPullStopped(ns))
1752
+ return;
859
1753
  if (!ns || !Number.isSafeInteger(seq) || seq <= 0)
860
1754
  return;
861
1755
  const pending = this.pendingGroupPullUpper ?? new Map();
@@ -863,6 +1757,8 @@ export class MessageDeliveryEngine {
863
1757
  pending.set(ns, Math.max(pending.get(ns) ?? 0, seq));
864
1758
  }
865
1759
  schedulePendingGroupPullIfNeeded(ns, reason) {
1760
+ if (this.automaticGroupPullStopped(ns))
1761
+ return false;
866
1762
  const pending = this.pendingGroupPullUpper;
867
1763
  if (!pending)
868
1764
  return false;
@@ -898,6 +1794,12 @@ export class MessageDeliveryEngine {
898
1794
  }
899
1795
  }
900
1796
  markPublishedSeq(ns, seq) {
1797
+ const page = this.currentApplicationPage();
1798
+ if (page) {
1799
+ page.consumed.add(JSON.stringify(['published', ns, seq]));
1800
+ page.commits.push(() => this.markPublishedSeq(ns, seq));
1801
+ return;
1802
+ }
901
1803
  const client = this.runtime.client;
902
1804
  let pushed = client._pushedSeqs.get(ns);
903
1805
  if (!pushed) {
@@ -910,6 +1812,18 @@ export class MessageDeliveryEngine {
910
1812
  client._pushedSeqs.set(ns, new Set(keep));
911
1813
  }
912
1814
  }
1815
+ hasPublishedSeq(ns, seq) {
1816
+ return this.runtime.client._pushedSeqs.get(ns)?.has(seq) === true
1817
+ || this.currentApplicationPage()?.consumed.has(JSON.stringify(['published', ns, seq])) === true;
1818
+ }
1819
+ consumeOrderedMessage(ns, seq, queue) {
1820
+ this.currentApplicationPage()?.consumed.add(JSON.stringify(['ordered', ns, seq]));
1821
+ this.completeApplicationDelivery(() => {
1822
+ queue?.delete(seq);
1823
+ if (queue?.size === 0)
1824
+ this.runtime.client._pendingOrderedMsgs.delete(ns);
1825
+ });
1826
+ }
913
1827
  enqueueOrderedMessage(ns, event, seq, payload, source = 'push') {
914
1828
  const client = this.runtime.client;
915
1829
  let queue = client._pendingOrderedMsgs.get(ns);
@@ -960,21 +1874,38 @@ export class MessageDeliveryEngine {
960
1874
  async publishOrderedGroupChanged(payload, source = 'ordered') {
961
1875
  const client = this.runtime.client;
962
1876
  const generation = this.captureInlineAckGeneration();
1877
+ const scope = this.applicationPage.getStore();
1878
+ const page = scope?.collecting ? scope : undefined;
1879
+ const owner = this.applicationClaims.get(this.applicationClaimKey('group.changed', payload, this.applicationNamespace('group.changed', payload), this.ensurePeerContext()));
1880
+ if (owner) {
1881
+ if (owner !== this.currentApplicationPage())
1882
+ await this.waitForApplicationOwner(owner);
1883
+ if (generation !== this.captureInlineAckGeneration())
1884
+ throw new Error('pull invalidated');
1885
+ return;
1886
+ }
963
1887
  const published = client._publishAppEvent('group.changed', payload, source);
964
1888
  if (isPromiseLike(published))
965
1889
  await published;
966
1890
  if (generation !== this.captureInlineAckGeneration())
967
1891
  throw new Error('pull invalidated');
968
- if (isJsonObject(payload)) {
969
- const eventPayload = payload;
970
- client._groupState?.handleGroupChangedV2Membership?.(eventPayload);
971
- if (eventPayload.action === 'dissolved') {
972
- const rawGroupId = eventPayload.group_id ?? eventPayload.group_aid ?? '';
973
- const groupId = String(rawGroupId).trim();
974
- if (groupId)
975
- client._cleanupDissolvedGroup?.(groupId);
1892
+ this.completeApplicationDelivery(() => {
1893
+ if (isJsonObject(payload)) {
1894
+ const eventPayload = payload;
1895
+ client._groupState?.handleGroupChangedV2Membership?.(eventPayload);
1896
+ if (eventPayload.action === 'dissolved') {
1897
+ const rawGroupId = eventPayload.group_id ?? eventPayload.group_aid ?? '';
1898
+ const groupId = String(rawGroupId).trim();
1899
+ if (groupId) {
1900
+ const cleanup = () => { client._cleanupDissolvedGroup?.(groupId); };
1901
+ if (page)
1902
+ page.cleanup.push(cleanup);
1903
+ else
1904
+ cleanup();
1905
+ }
1906
+ }
976
1907
  }
977
- }
1908
+ });
978
1909
  }
979
1910
  isInstanceScopedMessageEvent(event) {
980
1911
  return event === 'message.received'
@@ -1078,6 +2009,7 @@ export class MessageDeliveryEngine {
1078
2009
  setIfPresent('timestamp', firstValue(message.timestamp, message.created_at, message.t_server));
1079
2010
  if ('encrypted' in message)
1080
2011
  envelope.encrypted = Boolean(message.encrypted);
2012
+ envelope.need_render = message.need_render !== false;
1081
2013
  const context = this.envelopeMetadata(message.context);
1082
2014
  if (context)
1083
2015
  envelope.context = context;
@@ -1145,6 +2077,7 @@ export class MessageDeliveryEngine {
1145
2077
  setIfPresent('version', firstValue(body.version, params.version));
1146
2078
  setIfPresent('timestamp', firstValue(params.timestamp, resultObj.timestamp, resultObj.created_at, resultObj.t_server, Date.now()));
1147
2079
  envelope.encrypted = Boolean(encrypted);
2080
+ envelope.need_render = params.need_render !== false;
1148
2081
  const context = this.envelopeMetadata(params.context);
1149
2082
  if (context)
1150
2083
  envelope.context = context;
@@ -1306,12 +2239,14 @@ export class MessageDeliveryEngine {
1306
2239
  throw new Error('pull invalidated');
1307
2240
  if (this.isPullScopedSource(source))
1308
2241
  this.ensurePullOperationCurrent(`p2p:${client._aid ?? ''}`);
1309
- seen.set(dedupKey, Date.now());
1310
- if (seen.size > MESSAGE_RECALL_SEEN_LIMIT) {
1311
- const drop = [...seen.entries()].sort((a, b) => a[1] - b[1]).slice(0, seen.size - MESSAGE_RECALL_SEEN_LIMIT);
1312
- for (const [oldKey] of drop)
1313
- seen.delete(oldKey);
1314
- }
2242
+ this.completeApplicationDelivery(() => {
2243
+ seen.set(dedupKey, Date.now());
2244
+ if (seen.size > MESSAGE_RECALL_SEEN_LIMIT) {
2245
+ const drop = [...seen.entries()].sort((a, b) => a[1] - b[1]).slice(0, seen.size - MESSAGE_RECALL_SEEN_LIMIT);
2246
+ for (const [oldKey] of drop)
2247
+ seen.delete(oldKey);
2248
+ }
2249
+ });
1315
2250
  client._clientLog.debug(`message.recalled published: seq=${String(seq)} ids=${JSON.stringify(eventPayload.message_ids)}`);
1316
2251
  return true;
1317
2252
  }
@@ -1417,12 +2352,14 @@ export class MessageDeliveryEngine {
1417
2352
  throw new Error('pull invalidated');
1418
2353
  if (this.isPullScopedSource(source))
1419
2354
  this.ensurePullOperationCurrent(`group:${groupId}`);
1420
- seen.set(dedupKey, Date.now());
1421
- if (seen.size > GROUP_RECALL_SEEN_LIMIT) {
1422
- const drop = [...seen.entries()].sort((a, b) => a[1] - b[1]).slice(0, seen.size - GROUP_RECALL_SEEN_LIMIT);
1423
- for (const [oldKey] of drop)
1424
- seen.delete(oldKey);
1425
- }
2355
+ this.completeApplicationDelivery(() => {
2356
+ seen.set(dedupKey, Date.now());
2357
+ if (seen.size > GROUP_RECALL_SEEN_LIMIT) {
2358
+ const drop = [...seen.entries()].sort((a, b) => a[1] - b[1]).slice(0, seen.size - GROUP_RECALL_SEEN_LIMIT);
2359
+ for (const [oldKey] of drop)
2360
+ seen.delete(oldKey);
2361
+ }
2362
+ });
1426
2363
  client._clientLog.debug(`group.message_recalled published: group=${groupId} seq=${String(seq)} ids=${JSON.stringify(eventPayload.message_ids)}`);
1427
2364
  return true;
1428
2365
  }
@@ -1471,6 +2408,14 @@ export class MessageDeliveryEngine {
1471
2408
  }
1472
2409
  publishAppEvent(event, payload, source = 'direct', deliveryContext) {
1473
2410
  const client = this.runtime.client;
2411
+ const scope = this.applicationPage.getStore();
2412
+ const page = scope?.collecting ? scope : undefined;
2413
+ if (APPLICATION_DELIVERY_EVENTS.has(event) && typeof client._dispatcher._snapshotApplicationHandlers === 'function') {
2414
+ const namespace = this.applicationNamespace(event, payload);
2415
+ if (!page || (page.namespace !== namespace && !(page.namespace.startsWith('p2p:') && event.startsWith('message.')))) {
2416
+ return this.runApplicationPage(namespace, source, async () => { await this.publishAppEvent(event, payload, source, deliveryContext); });
2417
+ }
2418
+ }
1474
2419
  if ((event === 'message.received' || event === 'group.message_created') && isJsonObject(payload)) {
1475
2420
  client._maybeAppendEchoTraceReceive(payload);
1476
2421
  }
@@ -1491,22 +2436,33 @@ export class MessageDeliveryEngine {
1491
2436
  }
1492
2437
  const generation = this.deliveryChangedGeneration;
1493
2438
  const normalized = this.normalizePublishedMessagePayload(event, payload, source);
2439
+ const note = () => {
2440
+ if (generation === this.deliveryChangedGeneration) {
2441
+ this.noteDeliveryChanged(event, payload, source, deliveryContext);
2442
+ }
2443
+ };
2444
+ if (page && APPLICATION_DELIVERY_EVENTS.has(event)) {
2445
+ if (page.batching)
2446
+ return this.collectApplicationEvent(page, event, normalized, note);
2447
+ const owner = this.applicationClaims.get(this.applicationClaimKey(event, normalized, page.namespace, page.context));
2448
+ if (owner)
2449
+ return this.waitForApplicationOwner(owner).then(() => this.ensureApplicationPageCurrent(page));
2450
+ }
1494
2451
  const dispatcher = client._dispatcher;
1495
2452
  const result = ['message.received', 'group.message_created', 'message.recalled', 'group.message_recalled', 'group.changed'].includes(event)
1496
2453
  && typeof dispatcher._publishAndWait === 'function'
1497
- ? dispatcher._publishAndWait(event, normalized)
2454
+ ? dispatcher._publishAndWait(event, normalized, undefined, page?.reservation, page ? () => this.isApplicationPageCurrent(page) : undefined)
1498
2455
  : typeof dispatcher.enqueue === 'function'
1499
2456
  ? (dispatcher.enqueue(event, normalized), undefined)
1500
2457
  : typeof dispatcher.publishSyncAware === 'function'
1501
2458
  ? dispatcher.publishSyncAware(event, normalized)
1502
2459
  : dispatcher.publish?.(event, normalized);
1503
- const note = () => {
1504
- if (generation === this.deliveryChangedGeneration) {
1505
- this.noteDeliveryChanged(event, payload, source, deliveryContext);
1506
- }
1507
- };
1508
2460
  if (isPromiseLike(result))
1509
- return Promise.resolve(result).then(note);
2461
+ return Promise.resolve(result).then(() => {
2462
+ if (page && APPLICATION_DELIVERY_EVENTS.has(event))
2463
+ this.ensureApplicationPageCurrent(page);
2464
+ note();
2465
+ });
1510
2466
  note();
1511
2467
  return result;
1512
2468
  }
@@ -1703,6 +2659,10 @@ export class MessageDeliveryEngine {
1703
2659
  }
1704
2660
  async onRawMessageReceived(data) {
1705
2661
  const client = this.runtime.client;
2662
+ if (client._p2pPeerPullEnabled) {
2663
+ await this.onPeerPush(data);
2664
+ return;
2665
+ }
1706
2666
  const tStart = Date.now();
1707
2667
  if (isJsonObject(data)) {
1708
2668
  client._logMessageDebug('server-push', '_raw.message.received', 'message.received', data);
@@ -1745,6 +2705,11 @@ export class MessageDeliveryEngine {
1745
2705
  if (!this.messageTargetsCurrentInstance(msg))
1746
2706
  return;
1747
2707
  const seq = msg.seq;
2708
+ if (client._p2pPeerPullEnabled) {
2709
+ await this.onPeerPush(msg);
2710
+ await this.publishMessageRecallTombstone(seq, msg);
2711
+ return;
2712
+ }
1748
2713
  if (seq !== undefined && seq !== null && client._aid) {
1749
2714
  const ns = `p2p:${client._aid}`;
1750
2715
  const seqNum = Number(seq);
@@ -1933,20 +2898,26 @@ export class MessageDeliveryEngine {
1933
2898
  const afterSeq = client._seqTracker.getContiguousSeq(ns);
1934
2899
  client._clientLog.debug(`auto pull group messages start: group=${groupId}, after_seq=${afterSeq}, seq=${String(notification.seq ?? '')}`);
1935
2900
  const operation = async () => {
1936
- const messages = await client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true });
2901
+ const messages = await client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true, background: true });
1937
2902
  this.prunePushedSeqs(ns);
1938
2903
  return messages.length;
1939
2904
  };
1940
2905
  if (client._rpcPipeline) {
1941
2906
  const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 });
1942
- await client._rpcPipeline.runPullSerialized(key, operation, true);
2907
+ await this.runAutomaticPull(ns, operation, (attempt) => client._rpcPipeline.runPullNamespaceSerialized(key, attempt, true));
1943
2908
  }
1944
2909
  else {
1945
- await client._withBackgroundRpc(operation);
2910
+ await this.runAutomaticPull(ns, operation, (attempt) => client._withBackgroundRpc(attempt));
1946
2911
  }
1947
2912
  }
1948
2913
  async fillP2pGap() {
2914
+ if (!this.hasP2pApplicationSubscription())
2915
+ return;
1949
2916
  const client = this.runtime.client;
2917
+ if (client._p2pPeerPullEnabled) {
2918
+ this.refreshPendingPeers();
2919
+ return;
2920
+ }
1950
2921
  if (client._sessionOptions?.background_sync === false)
1951
2922
  return;
1952
2923
  if (!client._aid)
@@ -1966,9 +2937,9 @@ export class MessageDeliveryEngine {
1966
2937
  // Gap Fill 本身就是 Forward Pull,必须沿用协调器的完整事务:
1967
2938
  // 原始页提交成功后由协调器负责 Piggyback/终端 ACK。
1968
2939
  const operation = () => client._pullV2(afterSeq, 50, { gateLocked: true });
1969
- const messages = client._rpcPipeline
1970
- ? await client._rpcPipeline.runPullSerialized(client._rpcPipeline.pullGateKeyForCall('message.pull', { after_seq: afterSeq, limit: 50 }), operation, true)
1971
- : 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));
1972
2943
  filled = messages.length;
1973
2944
  this.prunePushedSeqs(ns);
1974
2945
  client._clientLog.debug(`P2P message gap fill done: after_seq=${afterSeq}, filled=${filled}`);
@@ -1993,6 +2964,8 @@ export class MessageDeliveryEngine {
1993
2964
  if (!groupId)
1994
2965
  return;
1995
2966
  const ns = `group:${groupId}`;
2967
+ if (this.automaticGroupPullStopped(ns))
2968
+ return;
1996
2969
  const afterSeq = client._seqTracker.getContiguousSeq(ns);
1997
2970
  const generation = this.captureInlineAckGeneration();
1998
2971
  const dedupKey = `group_msg:${groupId}:${afterSeq}`;
@@ -2004,10 +2977,10 @@ export class MessageDeliveryEngine {
2004
2977
  let filled = 0;
2005
2978
  let failed = false;
2006
2979
  try {
2007
- const operation = () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true });
2008
- const messages = client._rpcPipeline
2009
- ? await client._rpcPipeline.runPullSerialized(client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 }), operation, true)
2010
- : await client._withBackgroundRpc(operation);
2980
+ const operation = () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true, background: true });
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));
2011
2984
  filled = messages.length;
2012
2985
  this.prunePushedSeqs(ns);
2013
2986
  client._clientLog.debug(`group message gap fill done: group=${groupId}, after_seq=${afterSeq}, filled=${filled}`);
@@ -2063,6 +3036,8 @@ export class MessageDeliveryEngine {
2063
3036
  if (!groupId)
2064
3037
  return;
2065
3038
  const ns = `group_event:${groupId}`;
3039
+ if (this.automaticGroupPullStopped(ns))
3040
+ return;
2066
3041
  const generation = this.captureInlineAckGeneration();
2067
3042
  const requestedAfterSeq = Number(options.afterSeq ?? 0);
2068
3043
  const afterSeq = Number.isSafeInteger(requestedAfterSeq) && requestedAfterSeq > 0
@@ -2106,10 +3081,10 @@ export class MessageDeliveryEngine {
2106
3081
  let result;
2107
3082
  if (client._rpcPipeline) {
2108
3083
  const pullKey = client._rpcPipeline.pullGateKeyForCall('group.pull_events', request);
2109
- 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));
2110
3085
  }
2111
3086
  else {
2112
- result = await client.call('group.pull_events', request);
3087
+ result = await this.runAutomaticPull(ns, () => client.call('group.pull_events', request));
2113
3088
  }
2114
3089
  if (!isJsonObject(result))
2115
3090
  return;
@@ -2120,6 +3095,7 @@ export class MessageDeliveryEngine {
2120
3095
  if (!Array.isArray(events))
2121
3096
  return;
2122
3097
  const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
3098
+ const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
2123
3099
  let pageTrackerSnapshot = null;
2124
3100
  let trackerMutated = false;
2125
3101
  const eventObjects = events.filter((evt) => isJsonObject(evt));
@@ -2135,35 +3111,37 @@ export class MessageDeliveryEngine {
2135
3111
  const eventSeqs = [];
2136
3112
  let hasDissolvedEvent = false;
2137
3113
  let publishedEventCount = 0;
2138
- for (const evt of eventObjects) {
2139
- const eventSeq = Number(evt.event_seq ?? 0);
2140
- if (Number.isFinite(eventSeq) && eventSeq > 0)
2141
- eventSeqs.push(eventSeq);
2142
- evt._from_gap_fill = true;
2143
- if (evt.action === 'dissolved')
2144
- hasDissolvedEvent = true;
2145
- const et = String(evt.event_type ?? '');
2146
- if (et !== 'group.message_created') {
2147
- const cs = evt.client_signature;
2148
- if (cs && isJsonObject(cs)) {
2149
- if (client._shouldSkipEventSignature(evt)) {
2150
- delete evt.client_signature;
2151
- }
2152
- else {
2153
- evt._verified = await client._verifyEventSignatureAsync(evt, cs);
3114
+ try {
3115
+ await this.runApplicationPage(ns, 'pull', async () => {
3116
+ for (const evt of eventObjects) {
3117
+ const eventSeq = Number(evt.event_seq ?? 0);
3118
+ if (Number.isFinite(eventSeq) && eventSeq > 0)
3119
+ eventSeqs.push(eventSeq);
3120
+ evt._from_gap_fill = true;
3121
+ if (evt.action === 'dissolved')
3122
+ hasDissolvedEvent = true;
3123
+ const et = String(evt.event_type ?? '');
3124
+ if (et !== 'group.message_created') {
3125
+ const cs = evt.client_signature;
3126
+ if (cs && isJsonObject(cs)) {
3127
+ if (client._shouldSkipEventSignature(evt)) {
3128
+ delete evt.client_signature;
3129
+ }
3130
+ else {
3131
+ evt._verified = await client._verifyEventSignatureAsync(evt, cs);
3132
+ }
3133
+ }
3134
+ if (Number.isFinite(eventSeq) && eventSeq > 0 && !client._pushedSeqs.get(ns)?.has(eventSeq)) {
3135
+ this.enqueueOrderedMessage(ns, 'group.changed', eventSeq, evt, 'pull');
3136
+ publishedEventCount += 1;
3137
+ }
2154
3138
  }
3139
+ if (et !== 'group.message_created')
3140
+ filled += 1;
2155
3141
  }
2156
- if (Number.isFinite(eventSeq) && eventSeq > 0 && !client._pushedSeqs.get(ns)?.has(eventSeq)) {
2157
- this.enqueueOrderedMessage(ns, 'group.changed', eventSeq, evt, 'pull');
2158
- publishedEventCount += 1;
2159
- }
2160
- }
2161
- if (et !== 'group.message_created')
2162
- filled += 1;
2163
- }
2164
- try {
2165
- const scanTarget = Math.max(pageContigBefore, effectiveFloor, positiveSafeSequenceHint(result.contiguous_seq), ...eventSeqs);
2166
- await this.drainOrderedMessages(ns, undefined, false, false, 'pull', undefined, scanTarget);
3142
+ const scanTarget = Math.max(pageContigBefore, effectiveFloor, positiveSafeSequenceHint(result.contiguous_seq), ...eventSeqs);
3143
+ await this.drainOrderedMessages(ns, undefined, false, false, 'pull', undefined, scanTarget);
3144
+ });
2167
3145
  if (generation !== this.captureInlineAckGeneration())
2168
3146
  throw new Error('pull invalidated');
2169
3147
  this.ensurePullOperationCurrent(ns);
@@ -2171,6 +3149,9 @@ export class MessageDeliveryEngine {
2171
3149
  dissolved = true;
2172
3150
  break;
2173
3151
  }
3152
+ if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
3153
+ throw new Error('pull invalidated');
3154
+ this.rememberCommittedSeqState(ns);
2174
3155
  pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
2175
3156
  trackerMutated = true;
2176
3157
  client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq, Number.isSafeInteger(result.contiguous_seq)
@@ -2183,10 +3164,13 @@ export class MessageDeliveryEngine {
2183
3164
  this.ensurePullOperationCurrent(ns);
2184
3165
  if (generation !== this.captureInlineAckGeneration())
2185
3166
  throw new Error('pull invalidated');
3167
+ if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
3168
+ throw new Error('pull invalidated');
2186
3169
  }
2187
3170
  }
2188
3171
  catch (exc) {
2189
3172
  if (trackerMutated && generation === this.captureInlineAckGeneration() && this.isPullOperationCurrent(ns)
3173
+ && this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration)
2190
3174
  && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
2191
3175
  (client._seqTracker._restoreNamespaceSnapshotPreservingPush ?? client._seqTracker.restoreNamespaceSnapshot)
2192
3176
  .call(client._seqTracker, ns, pageTrackerSnapshot);
@@ -2269,6 +3253,8 @@ export class MessageDeliveryEngine {
2269
3253
  enqueueOnlineUnreadEventHint(data) {
2270
3254
  const client = this.runtime.client;
2271
3255
  const groupId = String(data.group_id ?? '').trim();
3256
+ if (this.automaticGroupPullStopped(`group_event:${groupId}`))
3257
+ return;
2272
3258
  if (!groupId || client._sessionOptions?.background_sync === false)
2273
3259
  return;
2274
3260
  this.recordBackgroundPullNeed(`group_event:${groupId}`, positiveSafeSequenceHint(data.event_seq), true);
@@ -2598,9 +3584,9 @@ export class MessageDeliveryEngine {
2598
3584
  return { messages, raw_count: messages.length };
2599
3585
  };
2600
3586
  if (!client._rpcPipeline)
2601
- return await invoke();
3587
+ return await this.runAutomaticPull(ns, invoke);
2602
3588
  const key = client._rpcPipeline.pullGateKeyForCall('message.pull', request);
2603
- return await client._rpcPipeline.runPullSerialized(key, invoke, background);
3589
+ return await this.runAutomaticPull(ns, invoke, (attempt) => client._rpcPipeline.runPullSerialized(key, attempt, background));
2604
3590
  };
2605
3591
  if (order.length === 1 && order[0] === 'tail') {
2606
3592
  result = await run({ window_mode: 'tail', after_seq: this.syncState(ns).ack, limit: pageLimit }, false);
@@ -2665,20 +3651,20 @@ export class MessageDeliveryEngine {
2665
3651
  window_mode: 'tail',
2666
3652
  after_seq: after,
2667
3653
  limit: pageLimit,
2668
- }, true);
3654
+ }, true, true);
2669
3655
  tailHead = this.syncState(ns).head;
2670
3656
  return tailResult;
2671
3657
  }
2672
- const opts = { gateLocked: true };
3658
+ const opts = { gateLocked: true, background };
2673
3659
  if (request.max_pages !== undefined)
2674
3660
  opts.maxPages = request.max_pages;
2675
3661
  const messages = await client._pullGroupV2(groupId, after, pageLimit, opts);
2676
3662
  return { messages, raw_count: messages.length };
2677
3663
  };
2678
3664
  if (!client._rpcPipeline)
2679
- return await invoke();
3665
+ return await this.runAutomaticPull(ns, invoke);
2680
3666
  const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, ...request });
2681
- return await client._rpcPipeline.runPullSerialized(key, invoke, background);
3667
+ return await this.runAutomaticPull(ns, invoke, (attempt) => client._rpcPipeline.runPullNamespaceSerialized(key, attempt, background));
2682
3668
  };
2683
3669
  if (order.length === 1 && order[0] === 'tail') {
2684
3670
  result = await run({ window_mode: 'tail', after_seq: this.syncState(ns).ack, limit: pageLimit }, false);
@@ -2719,9 +3705,9 @@ export class MessageDeliveryEngine {
2719
3705
  return { messages, raw_count: messages.length };
2720
3706
  };
2721
3707
  if (!client._rpcPipeline)
2722
- return await invoke();
3708
+ return await this.runAutomaticPull(ns, invoke);
2723
3709
  const key = client._rpcPipeline.pullGateKeyForCall('message.pull', request);
2724
- return await client._rpcPipeline.runPullSerialized(key, invoke, background);
3710
+ return await this.runAutomaticPull(ns, invoke, (attempt) => client._rpcPipeline.runPullSerialized(key, attempt, background));
2725
3711
  }
2726
3712
  async runGroupForwardRecovery(groupId, ns, pageLimit, background = false, throughSeq = 0) {
2727
3713
  const client = this.runtime.client;
@@ -2729,13 +3715,13 @@ export class MessageDeliveryEngine {
2729
3715
  const maxPages = Math.max(1, Math.ceil(Math.max(0, throughSeq - ack) / pageLimit));
2730
3716
  const request = { group_id: groupId, after_seq: ack, limit: pageLimit, max_pages: maxPages };
2731
3717
  const invoke = async () => {
2732
- const messages = await client._pullGroupV2(groupId, Number(request.after_seq), pageLimit, { gateLocked: true, maxPages });
3718
+ const messages = await client._pullGroupV2(groupId, Number(request.after_seq), pageLimit, { gateLocked: true, maxPages, background });
2733
3719
  return { messages, raw_count: messages.length };
2734
3720
  };
2735
3721
  if (!client._rpcPipeline)
2736
- return await invoke();
3722
+ return await this.runAutomaticPull(ns, invoke);
2737
3723
  const key = client._rpcPipeline.pullGateKeyForCall('group.pull', request);
2738
- return await client._rpcPipeline.runPullSerialized(key, invoke, background);
3724
+ return await this.runAutomaticPull(ns, invoke, (attempt) => client._rpcPipeline.runPullNamespaceSerialized(key, attempt, background));
2739
3725
  }
2740
3726
  syncState(ns) {
2741
3727
  const tracker = this.runtime.client._seqTracker;
@@ -3076,6 +4062,7 @@ export class MessageDeliveryEngine {
3076
4062
  return false;
3077
4063
  const client = this.runtime.client;
3078
4064
  const tracker = client._seqTracker;
4065
+ const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
3079
4066
  let snapshot = null;
3080
4067
  let trackerMutated = false;
3081
4068
  let delivered = false;
@@ -3092,6 +4079,9 @@ export class MessageDeliveryEngine {
3092
4079
  delivered = true;
3093
4080
  if (!this.inlineGenerationIsCurrent(generation))
3094
4081
  return false;
4082
+ if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
4083
+ return false;
4084
+ this.rememberCommittedSeqState(ns);
3095
4085
  snapshot = typeof tracker.snapshotNamespace === 'function' ? tracker.snapshotNamespace(ns) : null;
3096
4086
  trackerMutated = true;
3097
4087
  const needsPull = Boolean(tracker.onMessageSeq(ns, seq));
@@ -3110,13 +4100,14 @@ export class MessageDeliveryEngine {
3110
4100
  if (!this.inlineGenerationIsCurrent(generation))
3111
4101
  return false;
3112
4102
  await client._commitSeqTrackerState(ns);
3113
- if (!this.inlineGenerationIsCurrent(generation))
4103
+ if (!this.inlineGenerationIsCurrent(generation) || !this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
3114
4104
  return false;
3115
4105
  this.markPublishedSeq(ns, seq);
3116
4106
  return true;
3117
4107
  }
3118
4108
  catch (exc) {
3119
4109
  if (trackerMutated && this.inlineGenerationIsCurrent(generation)
4110
+ && this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration)
3120
4111
  && typeof tracker.restoreNamespaceSnapshot === 'function') {
3121
4112
  (tracker._restoreNamespaceSnapshotPreservingPush ?? tracker.restoreNamespaceSnapshot).call(tracker, ns, snapshot);
3122
4113
  }
@@ -3131,6 +4122,8 @@ export class MessageDeliveryEngine {
3131
4122
  const tracker = client._seqTracker;
3132
4123
  if (!this.inlineGenerationIsCurrent(generation) || !this.inlineStateIsCaughtUp(ns, seq))
3133
4124
  return false;
4125
+ const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
4126
+ this.rememberCommittedSeqState(ns);
3134
4127
  const snapshot = typeof tracker.snapshotNamespace === 'function' ? tracker.snapshotNamespace(ns) : null;
3135
4128
  try {
3136
4129
  const needsPull = Boolean(tracker.onMessageSeq(ns, seq));
@@ -3145,10 +4138,11 @@ export class MessageDeliveryEngine {
3145
4138
  throw new Error('inline realtime terminal cursor did not collapse after consume');
3146
4139
  }
3147
4140
  await client._commitSeqTrackerState(ns);
3148
- return this.inlineGenerationIsCurrent(generation);
4141
+ return this.inlineGenerationIsCurrent(generation) && this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration);
3149
4142
  }
3150
4143
  catch (exc) {
3151
4144
  if (this.inlineGenerationIsCurrent(generation)
4145
+ && this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration)
3152
4146
  && typeof tracker.restoreNamespaceSnapshot === 'function') {
3153
4147
  (tracker._restoreNamespaceSnapshotPreservingPush ?? tracker.restoreNamespaceSnapshot).call(tracker, ns, snapshot);
3154
4148
  }
@@ -3342,8 +4336,99 @@ export class MessageDeliveryEngine {
3342
4336
  }
3343
4337
  return 0;
3344
4338
  }
4339
+ async onPeerPush(data, expectedVersion) {
4340
+ const client = this.runtime.client;
4341
+ if (!isJsonObject(data) || !client._aid || !this.messageTargetsCurrentInstance(data))
4342
+ return;
4343
+ const head = data;
4344
+ if (String(head.kind ?? '') === 'message.online_unread_hint' || !head.sender_aid) {
4345
+ this.refreshPendingPeers();
4346
+ return;
4347
+ }
4348
+ if (head.cursor_scope !== 'peer') {
4349
+ this.refreshPendingPeers();
4350
+ return;
4351
+ }
4352
+ const sender = String(head.sender_aid);
4353
+ this.ensurePeerContext();
4354
+ this.peerPushVersions.set(this.peerNamespace(sender), {});
4355
+ if (!hasNonEmptyInlineMessage(head.inline_message) && !this.isVolatileMessage(head) && !this.hasP2pApplicationSubscription())
4356
+ return;
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
+ }
4367
+ const seq = positiveSafeSequenceHint(head.seq);
4368
+ if (this.isVolatileMessage(head)) {
4369
+ await client._v2E2EE.publishV2VolatileMessages([head], 'push', false);
4370
+ return;
4371
+ }
4372
+ if (!seq) {
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();
4378
+ return;
4379
+ }
4380
+ client._seqTracker.updateMaxSeen(ns, seq);
4381
+ const row = isJsonObject(head.inline_message) ? head.inline_message : null;
4382
+ const prev = nonNegativeSafeSequenceHint(head.prev_peer_seq);
4383
+ if (row && prev !== null && prev < seq && prev <= this.syncState(ns).ack
4384
+ && (!expectedVersion || this.inlineRowKind(row) === expectedVersion) && this.inlineHeadMatches(head, row, seq)) {
4385
+ const consumed = await client._tryRunInlineRealtime(ns, seq, () => this.runPeerPageSerialized(ns, async () => {
4386
+ if (generation !== this.captureInlineAckGeneration() || prev > this.syncState(ns).ack)
4387
+ return false;
4388
+ const decoded = await this.decodeInlineP2P(row);
4389
+ if (!decoded || !this.decodedInlineMatches(head, decoded, seq))
4390
+ return false;
4391
+ decoded.sender_aid = sender;
4392
+ decoded.need_render = typeof row.need_render === 'boolean' ? row.need_render : decoded.need_render !== false;
4393
+ const event = this.p2pAppEventForMessage(decoded);
4394
+ await this.publishPulledMessage(event.event, ns, seq, event.payload, false, 'inline_push');
4395
+ if (generation !== this.captureInlineAckGeneration())
4396
+ throw new Error('inline invalidated');
4397
+ const snapshot = client._seqTracker.snapshotNamespace(ns);
4398
+ try {
4399
+ client._seqTracker.onPullResult(ns, [{ seq }], this.syncState(ns).ack, seq);
4400
+ await this.commitSeqTrackerState(ns);
4401
+ }
4402
+ catch (error) {
4403
+ client._seqTracker._restoreNamespaceSnapshotPreservingPush(ns, snapshot);
4404
+ throw error;
4405
+ }
4406
+ await client._v2E2EE.ackPeerV2(sender, this.syncState(ns).ack);
4407
+ return true;
4408
+ })).catch((error) => {
4409
+ if (generation === this.captureInlineAckGeneration())
4410
+ this.schedulePeerRecovery(sender, seq);
4411
+ throw error;
4412
+ });
4413
+ if (consumed?.acquired && consumed.value)
4414
+ return;
4415
+ }
4416
+ if (seq <= this.syncState(ns).ack || !this.hasP2pApplicationSubscription())
4417
+ return;
4418
+ if (client._sessionOptions?.background_sync !== false) {
4419
+ this.schedulePeerRecovery(sender, seq);
4420
+ }
4421
+ else {
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 } } : {}) }));
4424
+ }
4425
+ }
3345
4426
  async onV2PushNotification(data, expectedVersion, inlineGeneration) {
3346
4427
  const client = this.runtime.client;
4428
+ if (client._p2pPeerPullEnabled) {
4429
+ await this.onPeerPush(data, expectedVersion);
4430
+ return;
4431
+ }
3347
4432
  const syncGeneration = this.captureInlineAckGeneration();
3348
4433
  if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
3349
4434
  return;
@@ -3399,6 +4484,8 @@ export class MessageDeliveryEngine {
3399
4484
  this.retryPendingP2PInlineAck(ns, pushSeq, 'covered inline push');
3400
4485
  return;
3401
4486
  }
4487
+ if (!this.hasP2pApplicationSubscription())
4488
+ return;
3402
4489
  this.recordBackgroundPullNeed(ns, pushSeq);
3403
4490
  const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
3404
4491
  if (pullActive || !this.beginRealtimeSync(ns)) {
@@ -3430,6 +4517,8 @@ export class MessageDeliveryEngine {
3430
4517
  if (!groupId)
3431
4518
  return;
3432
4519
  const ns = `group:${groupId}`;
4520
+ if (this.automaticGroupPullStopped(ns))
4521
+ return;
3433
4522
  const generation = this.captureInlineAckGeneration();
3434
4523
  const afterSeq = this.syncState(ns).ack;
3435
4524
  this.recordBackgroundPullNeed(ns, positiveSafeSequenceHint(data.seq));
@@ -3503,6 +4592,8 @@ export class MessageDeliveryEngine {
3503
4592
  return;
3504
4593
  }
3505
4594
  const ns = `group:${groupId}`;
4595
+ if (this.automaticGroupPullStopped(ns))
4596
+ return;
3506
4597
  let inlineConsumed = false;
3507
4598
  const pendingInlineAck = this.pendingGroupInlineAcks?.get(ns);
3508
4599
  const inlineBlockedByPendingAck = Boolean(pendingInlineAck && pendingInlineAck.seq !== seq);
@@ -3561,6 +4652,14 @@ export class MessageDeliveryEngine {
3561
4652
  const client = this.runtime.client;
3562
4653
  if (!client._aid)
3563
4654
  return;
4655
+ this.ensurePeerContext();
4656
+ const rememberPeers = () => {
4657
+ const state = client._seqTracker.exportSyncState?.() ?? {};
4658
+ for (const [ns, triplet] of Object.entries(state)) {
4659
+ if (this.isReliableSeqNamespace(ns))
4660
+ this.committedSeqTrackerStates.set(ns, { ...triplet });
4661
+ }
4662
+ };
3564
4663
  try {
3565
4664
  const loadSync = client._tokenStore.loadAllSyncStates;
3566
4665
  if (typeof loadSync === 'function' && typeof client._seqTracker.restoreSyncState === 'function') {
@@ -3568,6 +4667,7 @@ export class MessageDeliveryEngine {
3568
4667
  if (state && Object.keys(state).length > 0) {
3569
4668
  state = this.migrateSeqStateGroupIds(state);
3570
4669
  client._seqTracker.restoreSyncState(state);
4670
+ rememberPeers();
3571
4671
  return;
3572
4672
  }
3573
4673
  }
@@ -3577,6 +4677,7 @@ export class MessageDeliveryEngine {
3577
4677
  if (state && Object.keys(state).length > 0) {
3578
4678
  state = this.migrateSeqStateGroupIds(state);
3579
4679
  client._seqTracker.restoreState(state);
4680
+ rememberPeers();
3580
4681
  return;
3581
4682
  }
3582
4683
  }
@@ -3590,6 +4691,7 @@ export class MessageDeliveryEngine {
3590
4691
  client._seqTracker.restoreSyncState(state);
3591
4692
  else
3592
4693
  client._seqTracker.restoreState(state);
4694
+ rememberPeers();
3593
4695
  }
3594
4696
  }
3595
4697
  }
@@ -3610,6 +4712,10 @@ export class MessageDeliveryEngine {
3610
4712
  }
3611
4713
  enqueueOnlineUnreadP2pHint(data) {
3612
4714
  const client = this.runtime.client;
4715
+ if (client._p2pPeerPullEnabled) {
4716
+ this.refreshPendingPeers();
4717
+ return;
4718
+ }
3613
4719
  if (!client._aid || !client._v2Session)
3614
4720
  return;
3615
4721
  const ns = `p2p:${client._aid}`;
@@ -3632,6 +4738,16 @@ export class MessageDeliveryEngine {
3632
4738
  const state = typeof client._seqTracker.exportSyncState === 'function'
3633
4739
  ? client._seqTracker.exportSyncState()
3634
4740
  : Object.fromEntries(Object.entries(client._seqTracker.exportState()).map(([ns, seq]) => [ns, { ack: Number(seq), tail: Number(seq), head: Number(seq) }]));
4741
+ this.ensurePeerContext();
4742
+ for (const ns of Object.keys(state)) {
4743
+ if (!this.isReliableSeqNamespace(ns))
4744
+ continue;
4745
+ const committed = this.committedSeqTrackerStates.get(ns);
4746
+ if (committed)
4747
+ state[ns] = { ...committed };
4748
+ else if (ns.startsWith('p2p.peer.v1/'))
4749
+ delete state[ns];
4750
+ }
3635
4751
  const pending = this.seqTrackerPending();
3636
4752
  if (pending.size > 0 && client._seqTrackerPendingPersistContext === context) {
3637
4753
  const liveNamespaces = new Set(Object.keys(state));
@@ -3659,9 +4775,12 @@ export class MessageDeliveryEngine {
3659
4775
  if (forceSeq !== undefined) {
3660
4776
  client._seqTracker.forceContiguousSeq(ns, seq);
3661
4777
  }
3662
- const state = typeof client._seqTracker.getSyncState === 'function'
3663
- ? client._seqTracker.getSyncState(ns)
3664
- : { ack: seq, tail: seq, head: seq };
4778
+ const state = this.committedSeqTrackerStates.get(ns) ?? (ns.startsWith('p2p.peer.v1/') ? undefined
4779
+ : typeof client._seqTracker.getSyncState === 'function'
4780
+ ? client._seqTracker.getSyncState(ns)
4781
+ : { ack: seq, tail: seq, head: seq });
4782
+ if (!state)
4783
+ return;
3665
4784
  this.mergeSeqTrackerPending(context, { [ns]: state });
3666
4785
  this.scheduleSeqTrackerFlush();
3667
4786
  }
@@ -3676,12 +4795,16 @@ export class MessageDeliveryEngine {
3676
4795
  if (![state.ack, state.tail, state.head].some((value) => Number(value) > 0))
3677
4796
  return;
3678
4797
  const [aid, deviceId, slotId] = JSON.parse(context);
4798
+ const tracker = client._seqTracker;
4799
+ const namespaceGeneration = this.seqTrackerNamespaceGenerations.get(ns) ?? 0;
3679
4800
  const pendingBeforeCommit = this.takeSeqTrackerPending(context, ns);
3680
4801
  try {
3681
4802
  await this.writeSeqTrackerState(aid, deviceId, slotId, { [ns]: state }, true);
3682
4803
  }
3683
4804
  catch (exc) {
3684
- this.restoreSeqTrackerPending(context, ns, pendingBeforeCommit);
4805
+ if (client._seqTracker === tracker && (this.seqTrackerNamespaceGenerations.get(ns) ?? 0) === namespaceGeneration) {
4806
+ this.restoreSeqTrackerPending(context, ns, pendingBeforeCommit);
4807
+ }
3685
4808
  throw exc;
3686
4809
  }
3687
4810
  client._clientLog.debug(`seq_tracker atomic commit: ns=${ns} A/T/H=${JSON.stringify(state)} context=${context}`);
@@ -3788,7 +4911,37 @@ export class MessageDeliveryEngine {
3788
4911
  }
3789
4912
  }
3790
4913
  async writeSeqTrackerState(aid, deviceId, slotId, state, throwOnError = false) {
3791
- await this.enqueueSeqTrackerWrite(() => this.writeSeqTrackerStateNow(aid, deviceId, slotId, state, throwOnError));
4914
+ const context = JSON.stringify([aid, deviceId, slotId]);
4915
+ const tracker = this.runtime.client._seqTracker;
4916
+ const namespaceGenerations = new Map(Object.keys(state).map((ns) => [ns, this.seqTrackerNamespaceGenerations.get(ns) ?? 0]));
4917
+ await this.enqueueSeqTrackerWrite(async () => {
4918
+ const current = context === this.currentSeqTrackerContext() && tracker === this.runtime.client._seqTracker;
4919
+ if (throwOnError && !current)
4920
+ throw new Error('seq tracker commit invalidated');
4921
+ if (current)
4922
+ this.ensurePeerContext();
4923
+ const writing = { ...state };
4924
+ if (!throwOnError && current) {
4925
+ for (const ns of Object.keys(writing)) {
4926
+ if (!this.isReliableSeqNamespace(ns))
4927
+ continue;
4928
+ const committed = this.committedSeqTrackerStates.get(ns);
4929
+ if (committed)
4930
+ writing[ns] = { ...committed };
4931
+ else if (ns.startsWith('p2p.peer.v1/'))
4932
+ delete writing[ns];
4933
+ }
4934
+ }
4935
+ await this.writeSeqTrackerStateNow(aid, deviceId, slotId, writing, throwOnError);
4936
+ if (throwOnError && context === this.currentSeqTrackerContext() && tracker === this.runtime.client._seqTracker) {
4937
+ for (const [ns, triplet] of Object.entries(writing)) {
4938
+ if (this.isReliableSeqNamespace(ns)
4939
+ && (this.seqTrackerNamespaceGenerations.get(ns) ?? 0) === namespaceGenerations.get(ns)) {
4940
+ this.committedSeqTrackerStates.set(ns, { ...triplet });
4941
+ }
4942
+ }
4943
+ }
4944
+ });
3792
4945
  }
3793
4946
  enqueueSeqTrackerWrite(operation) {
3794
4947
  const run = this.seqTrackerWriteChain.then(operation, operation);
@@ -3841,10 +4994,17 @@ export class MessageDeliveryEngine {
3841
4994
  const client = this.runtime.client;
3842
4995
  if (!client._aid || !ns)
3843
4996
  return;
4997
+ this.ensurePeerContext();
3844
4998
  const aid = client._aid;
3845
4999
  const deviceId = client._deviceId;
3846
5000
  const slotId = client._slotId;
3847
5001
  const seq = client._seqTracker.getContiguousSeq(ns);
5002
+ if (seq <= 0) {
5003
+ this.seqTrackerNamespaceGenerations.set(ns, (this.seqTrackerNamespaceGenerations.get(ns) ?? 0) + 1);
5004
+ this.committedSeqTrackerStates.delete(ns);
5005
+ this.initializedPeers.delete(ns);
5006
+ this.peerServerConfirmedAcks.delete(ns);
5007
+ }
3848
5008
  this.dropSeqTrackerPending(ns);
3849
5009
  const saveSeq = client._tokenStore.saveSeq;
3850
5010
  if (seq > 0 && typeof saveSeq === 'function') {
@@ -3902,7 +5062,9 @@ export class MessageDeliveryEngine {
3902
5062
  if (next < 0)
3903
5063
  next = 0;
3904
5064
  if (ns) {
3905
- const committedAck = client._seqTracker.getContiguousSeq(ns);
5065
+ this.ensurePeerContext();
5066
+ const localAck = client._seqTracker.getContiguousSeq(ns);
5067
+ const committedAck = Math.min(localAck, this.committedSeqTrackerStates.get(ns)?.ack ?? localAck);
3906
5068
  if (next > committedAck) {
3907
5069
  client._clientLog.warn(`ack clamp: method=${method} ${field}=${original} > committed_ack=${committedAck}, clamp`);
3908
5070
  next = committedAck;
@@ -3959,6 +5121,8 @@ export class MessageDeliveryEngine {
3959
5121
  const queue = client._pendingOrderedMsgs.get(ns);
3960
5122
  if (!queue || queue.size === 0)
3961
5123
  return;
5124
+ const page = this.currentApplicationPage();
5125
+ let deliveredThrough = client._seqTracker.getContiguousSeq(ns);
3962
5126
  let delivered = false;
3963
5127
  const rootTrigger = this.deliveryChangedTrigger(source);
3964
5128
  const drainContexts = new Map();
@@ -3966,16 +5130,17 @@ export class MessageDeliveryEngine {
3966
5130
  while (true) {
3967
5131
  if (pullScoped)
3968
5132
  this.ensurePullOperationCurrent(ns);
3969
- const contig = client._seqTracker.getContiguousSeq(ns);
5133
+ const contig = page ? Math.max(deliveredThrough, client._seqTracker.getContiguousSeq(ns)) : client._seqTracker.getContiguousSeq(ns);
3970
5134
  const ready = [...queue.keys()]
3971
- .filter((seq) => seq <= Math.max(contig, throughSeq ?? contig) && (beforeSeq === undefined || seq < beforeSeq))
5135
+ .filter((seq) => !page?.consumed.has(JSON.stringify(['ordered', ns, seq]))
5136
+ && seq <= Math.max(contig, throughSeq ?? contig) && (beforeSeq === undefined || seq < beforeSeq))
3972
5137
  .sort((a, b) => a - b);
3973
5138
  let seq = ready[0];
3974
5139
  if (seq === undefined) {
3975
5140
  const nextSeq = Math.max(contig, throughSeq ?? contig) + 1;
3976
5141
  if (beforeSeq !== undefined && nextSeq >= beforeSeq)
3977
5142
  break;
3978
- if (!queue.has(nextSeq))
5143
+ if (!queue.has(nextSeq) || page?.consumed.has(JSON.stringify(['ordered', ns, nextSeq])))
3979
5144
  break;
3980
5145
  seq = nextSeq;
3981
5146
  }
@@ -3984,10 +5149,11 @@ export class MessageDeliveryEngine {
3984
5149
  const item = queue.get(seq);
3985
5150
  if (!item)
3986
5151
  continue;
3987
- if (client._pushedSeqs.get(ns)?.has(seq)) {
3988
- queue.delete(seq);
5152
+ if (this.hasPublishedSeq(ns, seq)) {
5153
+ this.consumeOrderedMessage(ns, seq, queue);
3989
5154
  client._clientLog.debug(`publish ordered drain skipped duplicate: ns=${ns}, seq=${seq}, event=${item.event}`);
3990
- client._markOrderedSeqDelivered(ns, seq);
5155
+ this.completeApplicationDelivery(() => client._markOrderedSeqDelivered(ns, seq));
5156
+ deliveredThrough = Math.max(deliveredThrough, seq);
3991
5157
  continue;
3992
5158
  }
3993
5159
  const itemSource = item.source ?? source;
@@ -4007,12 +5173,13 @@ export class MessageDeliveryEngine {
4007
5173
  throw new Error('pull invalidated');
4008
5174
  if (pullScoped)
4009
5175
  this.ensurePullOperationCurrent(ns);
4010
- queue.delete(seq);
5176
+ this.consumeOrderedMessage(ns, seq, queue);
4011
5177
  if (item.event === 'group.changed' && isJsonObject(item.payload)
4012
5178
  && item.payload.action === 'dissolved')
4013
5179
  break;
4014
5180
  this.markPublishedSeq(ns, seq);
4015
- client._markOrderedSeqDelivered(ns, seq);
5181
+ this.completeApplicationDelivery(() => client._markOrderedSeqDelivered(ns, seq));
5182
+ deliveredThrough = Math.max(deliveredThrough, seq);
4016
5183
  if (throughSeq !== undefined)
4017
5184
  throughSeq = Math.max(throughSeq, seq);
4018
5185
  delivered = true;
@@ -4122,6 +5289,87 @@ export class MessageDeliveryEngine {
4122
5289
  return true;
4123
5290
  }
4124
5291
  async publishPulledMessage(event, ns, seq, payload, persist = true, source = 'pull') {
5292
+ const client = this.runtime.client;
5293
+ const seqNum = Number(seq);
5294
+ if (!client._p2pPeerPullEnabled || event !== 'message.received' || source === 'history'
5295
+ || !Number.isSafeInteger(seqNum) || seqNum <= 0 || !payload || typeof payload !== 'object'
5296
+ || payload.storage === 'volatile') {
5297
+ return this.publishPulledMessageInternal(event, ns, seq, payload, persist, source);
5298
+ }
5299
+ const context = this.ensurePeerContext();
5300
+ const generation = this.captureInlineAckGeneration();
5301
+ const ensureCurrent = () => {
5302
+ if (context !== this.ensurePeerContext() || generation !== this.captureInlineAckGeneration())
5303
+ throw new Error('delivery invalidated');
5304
+ if (this.isPullScopedSource(source))
5305
+ this.ensurePullOperationCurrent(ns);
5306
+ };
5307
+ const value = payload;
5308
+ const sender = String(value.sender_aid ?? value.from_aid ?? value.from ?? this.peerSender(ns));
5309
+ if (!sender)
5310
+ return this.publishPulledMessageInternal(event, ns, seq, payload, persist, source);
5311
+ const messageId = String(value.message_id ?? '');
5312
+ const key = JSON.stringify([context, event, seqNum]);
5313
+ const page = this.currentApplicationPage();
5314
+ const bindingMatches = (receipt) => receipt.sender_aid === sender
5315
+ && (!receipt.message_id || !messageId || receipt.message_id === messageId);
5316
+ const local = page?.bindings.get(key);
5317
+ if (local) {
5318
+ if (!bindingMatches(local))
5319
+ throw new Error('P2P delivery receipt binding conflict');
5320
+ if (messageId && !local.message_id)
5321
+ local.message_id = messageId;
5322
+ return false;
5323
+ }
5324
+ const pending = this.deliveryInflight.get(key);
5325
+ if (pending) {
5326
+ if (client._dispatcher._isApplicationHandler?.())
5327
+ throw new Error('delivery cannot wait inside an application handler');
5328
+ await pending;
5329
+ ensureCurrent();
5330
+ const settled = this.settledDeliveries.get(key);
5331
+ if (settled && !bindingMatches(settled))
5332
+ throw new Error('P2P delivery receipt binding conflict');
5333
+ if (!messageId || settled?.message_id)
5334
+ return false;
5335
+ }
5336
+ const operation = Promise.resolve().then(async () => {
5337
+ ensureCurrent();
5338
+ let settled = this.settledDeliveries.get(key);
5339
+ if (settled && !bindingMatches(settled))
5340
+ throw new Error('P2P delivery receipt binding conflict');
5341
+ if (settled && messageId && !settled.message_id) {
5342
+ settled = { ...settled, message_id: messageId };
5343
+ }
5344
+ let published = false;
5345
+ if (!settled) {
5346
+ if (!this.isSelfSentP2PMessage(payload)) {
5347
+ await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, source));
5348
+ published = true;
5349
+ }
5350
+ ensureCurrent();
5351
+ settled = { sender_aid: sender, ...(messageId ? { message_id: messageId } : {}) };
5352
+ }
5353
+ ensureCurrent();
5354
+ const binding = settled;
5355
+ page?.bindings.set(key, binding);
5356
+ this.completeApplicationDelivery(() => {
5357
+ this.settledDeliveries.set(key, binding);
5358
+ while (this.settledDeliveries.size > PUSHED_SEQS_LIMIT) {
5359
+ const first = this.settledDeliveries.keys().next().value;
5360
+ this.settledDeliveries.delete(first);
5361
+ }
5362
+ });
5363
+ this.markPublishedSeq(ns, seqNum);
5364
+ return published;
5365
+ });
5366
+ const completion = page ? operation.then(async (result) => { await page.completion; return result; }) : operation;
5367
+ this.deliveryInflight.set(key, completion);
5368
+ void completion.finally(() => { if (this.deliveryInflight.get(key) === completion)
5369
+ this.deliveryInflight.delete(key); }).catch(() => { });
5370
+ return await operation;
5371
+ }
5372
+ async publishPulledMessageInternal(event, ns, seq, payload, persist = true, source = 'pull') {
4125
5373
  const client = this.runtime.client;
4126
5374
  const generation = this.captureInlineAckGeneration();
4127
5375
  const pullScoped = this.isPullScopedSource(source);
@@ -4144,9 +5392,9 @@ export class MessageDeliveryEngine {
4144
5392
  return true;
4145
5393
  }
4146
5394
  const queue = client._pendingOrderedMsgs.get(ns);
4147
- if (client._pushedSeqs.get(ns)?.has(seqNum)) {
5395
+ if (this.hasPublishedSeq(ns, seqNum)) {
4148
5396
  client._clientLog.debug(`publish pulled skipped duplicate: event=${event}, ns=${ns}, seq=${seqNum}`);
4149
- queue?.delete(seqNum);
5397
+ this.consumeOrderedMessage(ns, seqNum, queue);
4150
5398
  if (queue && queue.size === 0)
4151
5399
  client._pendingOrderedMsgs.delete(ns);
4152
5400
  return false;
@@ -4165,7 +5413,7 @@ export class MessageDeliveryEngine {
4165
5413
  throw new Error('pull invalidated');
4166
5414
  if (pullScoped)
4167
5415
  this.ensurePullOperationCurrent(ns);
4168
- queue?.delete(seqNum);
5416
+ this.consumeOrderedMessage(ns, seqNum, queue);
4169
5417
  if (queue && queue.size === 0)
4170
5418
  client._pendingOrderedMsgs.delete(ns);
4171
5419
  this.markPublishedSeq(ns, seqNum);
@@ -4182,7 +5430,7 @@ export class MessageDeliveryEngine {
4182
5430
  throw new Error('pull invalidated');
4183
5431
  if (pullScoped)
4184
5432
  this.ensurePullOperationCurrent(ns);
4185
- queue?.delete(seqNum);
5433
+ this.consumeOrderedMessage(ns, seqNum, queue);
4186
5434
  if (queue && queue.size === 0)
4187
5435
  client._pendingOrderedMsgs.delete(ns);
4188
5436
  this.markPublishedSeq(ns, seqNum);