@agentunion/fastaun 0.5.20 → 0.5.21

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 (107) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/_packed_docs/CHANGELOG.md +47 -0
  3. package/_packed_docs/INDEX.md +453 -440
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +164 -161
  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/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1433 -1392
  10. package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
  11. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +2 -0
  12. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +666 -658
  13. 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
  14. 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
  15. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
  16. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
  17. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
  18. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +4 -1
  19. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +42 -39
  20. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +147 -14
  21. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +48 -47
  22. package/_packed_docs/sdk/09-group-rpc-manual.md +5 -2
  23. package/_packed_docs/sdk/09-message-rpc-manual.md +250 -160
  24. package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +71 -0
  25. package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +88 -0
  26. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +134 -130
  27. package/_packed_docs/sdk/INDEX.md +239 -221
  28. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +930 -922
  29. 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 +106 -43
  30. 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 +180 -9
  31. 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
  32. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +480 -476
  33. 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 +1 -1
  34. 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 +2 -2
  35. package/dist/agent-md-schema.js +6 -4
  36. package/dist/agent-md-schema.js.map +1 -1
  37. package/dist/agent-md.d.ts +22 -2
  38. package/dist/agent-md.js +84 -67
  39. package/dist/agent-md.js.map +1 -1
  40. package/dist/aid-store.d.ts +13 -0
  41. package/dist/aid-store.js +50 -15
  42. package/dist/aid-store.js.map +1 -1
  43. package/dist/auth.js +4 -0
  44. package/dist/auth.js.map +1 -1
  45. package/dist/cert-utils.d.ts +1 -0
  46. package/dist/cert-utils.js +13 -0
  47. package/dist/cert-utils.js.map +1 -1
  48. package/dist/client/delivery.d.ts +59 -1
  49. package/dist/client/delivery.js +1018 -125
  50. package/dist/client/delivery.js.map +1 -1
  51. package/dist/client/rpc-pipeline.d.ts +4 -1
  52. package/dist/client/rpc-pipeline.js +123 -37
  53. package/dist/client/rpc-pipeline.js.map +1 -1
  54. package/dist/client/v2-e2ee.d.ts +12 -2
  55. package/dist/client/v2-e2ee.js +751 -294
  56. package/dist/client/v2-e2ee.js.map +1 -1
  57. package/dist/client.d.ts +26 -2
  58. package/dist/client.js +379 -63
  59. package/dist/client.js.map +1 -1
  60. package/dist/config.d.ts +4 -0
  61. package/dist/config.js +1 -0
  62. package/dist/config.js.map +1 -1
  63. package/dist/events.d.ts +18 -1
  64. package/dist/events.js +148 -9
  65. package/dist/events.js.map +1 -1
  66. package/dist/facades.d.ts +43 -1
  67. package/dist/facades.js +363 -7
  68. package/dist/facades.js.map +1 -1
  69. package/dist/index.d.ts +5 -3
  70. package/dist/index.js +1 -1
  71. package/dist/index.js.map +1 -1
  72. package/dist/keystore/aid-db.d.ts +6 -1
  73. package/dist/keystore/aid-db.js +184 -42
  74. package/dist/keystore/aid-db.js.map +1 -1
  75. package/dist/keystore/index.d.ts +14 -0
  76. package/dist/keystore/local-token-store.d.ts +9 -1
  77. package/dist/keystore/local-token-store.js +79 -14
  78. package/dist/keystore/local-token-store.js.map +1 -1
  79. package/dist/org-protocol.d.ts +45 -0
  80. package/dist/org-protocol.js +754 -11
  81. package/dist/org-protocol.js.map +1 -1
  82. package/dist/ownership-v21.d.ts +16 -3
  83. package/dist/ownership-v21.js +35 -17
  84. package/dist/ownership-v21.js.map +1 -1
  85. package/dist/ownership-workflow.d.ts +17 -0
  86. package/dist/ownership-workflow.js +511 -26
  87. package/dist/ownership-workflow.js.map +1 -1
  88. package/dist/secret-store/file-store.js +84 -0
  89. package/dist/secret-store/file-store.js.map +1 -1
  90. package/dist/seq-tracker.d.ts +1 -0
  91. package/dist/seq-tracker.js +2 -2
  92. package/dist/seq-tracker.js.map +1 -1
  93. package/dist/tools/cross-sdk-agent.d.ts +66 -3
  94. package/dist/tools/cross-sdk-agent.js +466 -36
  95. package/dist/tools/cross-sdk-agent.js.map +1 -1
  96. package/dist/transport.d.ts +3 -0
  97. package/dist/transport.js +98 -44
  98. package/dist/transport.js.map +1 -1
  99. package/dist/types.d.ts +1 -0
  100. package/dist/v2/e2ee/encrypt-group.js +1 -0
  101. package/dist/v2/e2ee/encrypt-group.js.map +1 -1
  102. package/dist/v2/e2ee/encrypt-p2p.js +1 -0
  103. package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
  104. package/dist/v2/e2ee/types.d.ts +1 -0
  105. package/dist/version.d.ts +1 -1
  106. package/dist/version.js +1 -1
  107. package/package.json +1 -1
@@ -1,4 +1,6 @@
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';
3
5
  import { ConnectionState, isJsonObject } from '../types.js';
4
6
  import { validateGroupAIDFormat } from '../validators.js';
@@ -14,12 +16,13 @@ const DELIVERY_CHANGED_MESSAGE_EVENTS = new Set([
14
16
  'message.received',
15
17
  'group.message_created',
16
18
  ]);
19
+ const APPLICATION_DELIVERY_EVENTS = new Set(['message.received', 'message.recalled', 'group.message_created', 'group.message_recalled', 'group.changed']);
17
20
  const MAX_INLINE_PENDING_ACK_NAMESPACES = 4096;
18
21
  const APP_MESSAGE_ENVELOPE_KEYS = [
19
22
  'module_id', 'message_type', 'type', 'kind', 'version',
20
23
  'from', 'from_aid', 'sender_aid', 'to', 'to_aid', 'group_id',
21
24
  'timestamp', 'created_at', 'encrypted',
22
- 'context', 'protected_headers', 'headers', 'payload_type', 'mention_mode',
25
+ 'context', 'protected_headers', 'headers', 'payload_type', 'mention_mode', 'need_render',
23
26
  ];
24
27
  const RECALL_PAYLOAD_KEYS = [
25
28
  'message_ids', 'target_message_seqs',
@@ -100,9 +103,488 @@ export class MessageDeliveryEngine {
100
103
  volatileInFlightMessages = new Set();
101
104
  // 同一实例的 tracker 写入必须串行,避免旧的延迟 flush 覆盖新的 Forward 提交。
102
105
  seqTrackerWriteChain = Promise.resolve();
106
+ peerCatalogTask = null;
107
+ peerCatalogDirty = false;
108
+ peerCatalogQueries = new Map();
109
+ peerContext = '';
110
+ peerTracker = null;
111
+ peerContextGeneration = 0;
112
+ peerBootstrap = null;
113
+ initializedPeers = new Set();
114
+ committedSeqTrackerStates = new Map();
115
+ seqTrackerNamespaceGenerations = new Map();
116
+ peerRecoveryTasks = new Map();
117
+ peerRecoveryNeeds = new Map();
118
+ peerPullHints = new Map();
119
+ peerPullConfirmedThrough = new Map();
120
+ peerVolatileCursors = new Map();
121
+ settledDeliveries = new Map();
122
+ deliveryInflight = new Map();
123
+ applicationPage = new AsyncLocalStorage();
124
+ applicationClaims = new Map();
125
+ volatileCompletions = new Map();
126
+ peerPageChains = new Map();
103
127
  constructor(runtime) {
104
128
  this.runtime = runtime;
105
129
  }
130
+ currentApplicationPage() {
131
+ const page = this.applicationPage.getStore();
132
+ return page?.collecting && page.batching ? page : undefined;
133
+ }
134
+ isApplicationPageCurrent(page) {
135
+ return page.context === this.ensurePeerContext() && page.generation === this.captureInlineAckGeneration()
136
+ && this.isSeqNamespaceGenerationCurrent(page.namespace, page.namespaceGeneration)
137
+ && (!this.isPullScopedSource(page.source) || this.isPullOperationCurrent(page.namespace));
138
+ }
139
+ ensureApplicationPageCurrent(page) {
140
+ if (!this.isApplicationPageCurrent(page))
141
+ throw new Error(this.isPullScopedSource(page.source) ? 'pull invalidated' : 'delivery invalidated');
142
+ }
143
+ snapshotApplicationHandlers() {
144
+ return this.runtime.client._dispatcher._snapshotApplicationHandlers?.(['message.batch', 'group.message_batch', 'group.event_batch']);
145
+ }
146
+ async runApplicationPage(namespace, source, operation, reservation, snapshot) {
147
+ const dispatcher = this.runtime.client._dispatcher;
148
+ if (typeof dispatcher._snapshotApplicationHandlers !== 'function')
149
+ return await operation();
150
+ const previous = this.applicationPage.getStore();
151
+ if (previous?.collecting && previous.namespace === namespace)
152
+ return await operation();
153
+ let resolve, reject;
154
+ const completion = new Promise((done, fail) => { resolve = done; reject = fail; });
155
+ void completion.catch(() => { });
156
+ const handlers = snapshot ?? this.snapshotApplicationHandlers(), batchEvent = namespace.startsWith('group_event:') ? 'group.event_batch'
157
+ : namespace.startsWith('group:') ? 'group.message_batch' : 'message.batch';
158
+ const page = { namespace, source, context: this.ensurePeerContext(), generation: this.captureInlineAckGeneration(),
159
+ namespaceGeneration: this.captureSeqNamespaceGeneration(namespace),
160
+ collecting: true, batching: (handlers.get(batchEvent)?.length ?? 0) > 0, handlers, reservation,
161
+ items: [], commits: [], afterDelivery: [], cleanup: [], claims: new Set(), bindings: new Map(), volatileSeen: new Set(), consumed: new Set(),
162
+ completion, resolve, reject };
163
+ return await this.applicationPage.run(page, async () => {
164
+ try {
165
+ const result = await operation();
166
+ page.collecting = false;
167
+ this.ensureApplicationPageCurrent(page);
168
+ if (page.items.length > 0) {
169
+ const batchHandlers = page.handlers.get(batchEvent) ?? [];
170
+ await dispatcher._publishAndWait(batchEvent, { namespace, items: page.items, source }, batchHandlers, reservation, () => this.isApplicationPageCurrent(page));
171
+ }
172
+ this.ensureApplicationPageCurrent(page);
173
+ for (const commit of page.commits)
174
+ commit();
175
+ for (const action of page.afterDelivery) {
176
+ await action();
177
+ this.ensureApplicationPageCurrent(page);
178
+ }
179
+ for (const cleanup of page.cleanup)
180
+ cleanup();
181
+ page.resolve();
182
+ return result;
183
+ }
184
+ catch (error) {
185
+ page.reject(error);
186
+ throw error;
187
+ }
188
+ finally {
189
+ page.collecting = false;
190
+ for (const key of page.claims)
191
+ if (this.applicationClaims.get(key) === page)
192
+ this.applicationClaims.delete(key);
193
+ }
194
+ });
195
+ }
196
+ completeApplicationDelivery(operation) {
197
+ const page = this.currentApplicationPage();
198
+ if (page)
199
+ page.commits.push(operation);
200
+ else
201
+ operation();
202
+ }
203
+ applicationNamespace(event, payload) {
204
+ const value = payload && typeof payload === 'object' ? payload : {};
205
+ if (event === 'group.changed')
206
+ return `group_event:${String(value.group_id ?? value.group_aid ?? '')}`;
207
+ if (event.startsWith('group.'))
208
+ return `group:${String(value.group_id ?? value.group_aid ?? '')}`;
209
+ const sender = String(value.sender_aid ?? value.from_aid ?? value.from ?? '');
210
+ return this.runtime.client._p2pPeerPullEnabled && sender ? this.peerNamespace(sender) : `p2p:${this.runtime.client._aid ?? ''}`;
211
+ }
212
+ applicationClaimKey(event, payload, namespace, context) {
213
+ const value = payload && typeof payload === 'object' ? payload : {};
214
+ const seq = Number(event === 'group.changed' ? value.event_seq : value.seq);
215
+ const identity = event === 'message.recalled' ? this.messageRecallDedupKey(value)
216
+ : event === 'group.message_recalled' ? this.groupRecallDedupKey(String(value.group_id ?? ''), value)
217
+ : Number.isSafeInteger(seq) && seq > 0 ? seq : value.volatile_cursor ?? value.message_id;
218
+ return identity === undefined ? '' : JSON.stringify([context, event, event.startsWith('message.') ? 'p2p' : namespace,
219
+ value.storage === 'volatile' ? String(value.sender_aid ?? value.from_aid ?? value.from ?? '') : '', identity]);
220
+ }
221
+ async waitForApplicationOwner(owner) {
222
+ if (this.runtime.client._dispatcher._isApplicationHandler?.())
223
+ throw new Error('delivery cannot wait inside an application handler');
224
+ await owner.completion;
225
+ }
226
+ async collectApplicationEvent(page, event, payload, completed) {
227
+ this.ensureApplicationPageCurrent(page);
228
+ const key = this.applicationClaimKey(event, payload, page.namespace, page.context);
229
+ if (key) {
230
+ if (page.claims.has(key))
231
+ return;
232
+ const owner = this.applicationClaims.get(key);
233
+ if (owner) {
234
+ await this.waitForApplicationOwner(owner);
235
+ this.ensureApplicationPageCurrent(page);
236
+ return;
237
+ }
238
+ this.applicationClaims.set(key, page);
239
+ page.claims.add(key);
240
+ }
241
+ page.items.push({ event, payload });
242
+ page.commits.push(completed);
243
+ }
244
+ peerNamespace(senderAid) {
245
+ if (!senderAid.trim())
246
+ throw new Error('sender_aid is required');
247
+ return `p2p.peer.v1/${Buffer.from(senderAid, 'utf8').toString('base64url')}`;
248
+ }
249
+ async runPeerPageSerialized(namespace, operation) {
250
+ const previous = this.peerPageChains.get(namespace) ?? Promise.resolve();
251
+ const current = previous.catch(() => undefined).then(operation);
252
+ this.peerPageChains.set(namespace, current);
253
+ try {
254
+ return await current;
255
+ }
256
+ finally {
257
+ if (this.peerPageChains.get(namespace) === current)
258
+ this.peerPageChains.delete(namespace);
259
+ if (this.peerCatalogDirty && this.peerPageChains.size === 0 && this.peerRecoveryTasks.size === 0)
260
+ this.refreshPendingPeers();
261
+ }
262
+ }
263
+ hasPeerPageActivity(namespace) {
264
+ return this.peerPageChains.has(namespace);
265
+ }
266
+ peerAckChanged() {
267
+ this.requestBackgroundPullRecovery();
268
+ }
269
+ peerPullPriority(namespace) {
270
+ this.ensurePeerContext();
271
+ return this.peerPullHints.get(namespace) ?? Math.max(this.peerPullConfirmedThrough.get(namespace) ?? 0, this.syncState(namespace).ack);
272
+ }
273
+ consumePeerPullHint(namespace, coveredThroughSeq, nextPendingSeq) {
274
+ const hint = this.peerPullPriority(namespace);
275
+ if (coveredThroughSeq < (this.peerPullConfirmedThrough.get(namespace) ?? 0))
276
+ return;
277
+ this.peerPullConfirmedThrough.set(namespace, coveredThroughSeq);
278
+ if (hint !== null && hint <= coveredThroughSeq)
279
+ this.peerPullHints.delete(namespace);
280
+ if (nextPendingSeq !== undefined && nextPendingSeq > coveredThroughSeq) {
281
+ this.peerPullHints.set(namespace, Math.max(this.peerPullHints.get(namespace) ?? 0, nextPendingSeq));
282
+ }
283
+ }
284
+ peerSender(namespace) {
285
+ return namespace.startsWith('p2p.peer.v1/') ? Buffer.from(namespace.slice(12), 'base64url').toString('utf8') : '';
286
+ }
287
+ ensurePeerContext() {
288
+ const client = this.runtime.client;
289
+ const context = JSON.stringify([client._aid ?? '', client._deviceId ?? '', slotIsolationKey(client._slotId)]);
290
+ if (context !== this.peerContext || client._seqTracker !== this.peerTracker) {
291
+ this.peerContext = context;
292
+ this.peerTracker = client._seqTracker;
293
+ this.peerContextGeneration += 1;
294
+ this.peerBootstrap = null;
295
+ this.initializedPeers.clear();
296
+ this.committedSeqTrackerStates.clear();
297
+ this.seqTrackerNamespaceGenerations.clear();
298
+ this.peerVolatileCursors.clear();
299
+ this.peerRecoveryNeeds.clear();
300
+ this.peerPullHints.clear();
301
+ this.peerPullConfirmedThrough.clear();
302
+ this.peerRecoveryTasks.clear();
303
+ this.settledDeliveries.clear();
304
+ this.deliveryInflight.clear();
305
+ this.peerCatalogTask = null;
306
+ this.peerCatalogDirty = false;
307
+ this.peerCatalogQueries.clear();
308
+ }
309
+ return `${context}/${this.peerContextGeneration}`;
310
+ }
311
+ peerOwnType() {
312
+ const client = this.runtime.client;
313
+ try {
314
+ const current = client._currentAid;
315
+ if (!current?.isCertValid() || current.aid.toLowerCase() !== String(client._aid ?? '').toLowerCase())
316
+ return '';
317
+ const content = client._agentMdManager?.readContent?.(client._aid);
318
+ if (typeof content !== 'string' || !content)
319
+ return '';
320
+ const verified = current.verifyAgentMd(content);
321
+ if (!verified.ok || verified.data.status !== 'verified')
322
+ return '';
323
+ const type = parseFileFrontmatter(verified.data.payload).type;
324
+ return typeof type === 'string' ? type.trim() : '';
325
+ }
326
+ catch {
327
+ return '';
328
+ }
329
+ }
330
+ async initializePeer(senderAid, initialAfterSeq) {
331
+ const client = this.runtime.client;
332
+ const context = this.ensurePeerContext();
333
+ const ns = this.peerNamespace(senderAid);
334
+ if (this.initializedPeers.has(ns)) {
335
+ if (client._seqTracker.snapshotNamespace(ns) !== null)
336
+ return ns;
337
+ this.initializedPeers.delete(ns);
338
+ this.committedSeqTrackerStates.delete(ns);
339
+ }
340
+ const hadState = client._seqTracker.snapshotNamespace(ns) !== null;
341
+ if (this.peerBootstrap === null)
342
+ await this.peers({}, true);
343
+ if (context !== this.ensurePeerContext())
344
+ throw new Error('peer initialization invalidated');
345
+ if (this.initializedPeers.has(ns))
346
+ return ns;
347
+ if (!hadState) {
348
+ if (initialAfterSeq === undefined) {
349
+ let pageToken;
350
+ do {
351
+ const catalog = await this.peers({ filter: 'all', ...(pageToken ? { page_token: pageToken } : {}) }, true);
352
+ if (context !== this.ensurePeerContext())
353
+ throw new Error('peer initialization invalidated');
354
+ if (this.initializedPeers.has(ns))
355
+ return ns;
356
+ pageToken = catalog.has_more === true ? String(catalog.next_page_token ?? '') : undefined;
357
+ if (catalog.has_more === true && !pageToken)
358
+ throw new Error('all peer catalog is incomplete');
359
+ } while (pageToken);
360
+ }
361
+ const initial = initialAfterSeq ?? this.peerBootstrap ?? 0;
362
+ if (!Number.isSafeInteger(initial) || initial < 0)
363
+ throw new Error('invalid peer initialization baseline');
364
+ client._seqTracker.forceContiguousSeq(ns, initial);
365
+ }
366
+ const state = { ...client._seqTracker.getSyncState(ns) };
367
+ await this.writeSeqTrackerState(client._aid, client._deviceId, client._slotId, { [ns]: state }, true);
368
+ if (context !== this.ensurePeerContext())
369
+ throw new Error('peer initialization invalidated');
370
+ this.committedSeqTrackerStates.set(ns, state);
371
+ this.initializedPeers.add(ns);
372
+ return ns;
373
+ }
374
+ peerCommittedAck(namespace) {
375
+ this.ensurePeerContext();
376
+ return Math.min(this.committedSeqTrackerStates.get(namespace)?.ack ?? 0, this.syncState(namespace).ack);
377
+ }
378
+ rememberCommittedSeqState(namespace) {
379
+ this.ensurePeerContext();
380
+ if (this.isReliableSeqNamespace(namespace) && !this.committedSeqTrackerStates.has(namespace)) {
381
+ this.committedSeqTrackerStates.set(namespace, { ...this.syncState(namespace) });
382
+ }
383
+ }
384
+ isReliableSeqNamespace(namespace) {
385
+ return namespace.startsWith('p2p.peer.v1/') || namespace.startsWith('group:') || namespace.startsWith('group_event:');
386
+ }
387
+ captureSeqNamespaceGeneration(namespace) {
388
+ return JSON.stringify([this.ensurePeerContext(), this.seqTrackerNamespaceGenerations.get(namespace) ?? 0]);
389
+ }
390
+ isSeqNamespaceGenerationCurrent(namespace, generation) {
391
+ return this.captureSeqNamespaceGeneration(namespace) === generation;
392
+ }
393
+ async peers(params = {}, background = false) {
394
+ const context = this.ensurePeerContext();
395
+ const key = JSON.stringify([context, params.filter ?? 'pending', params.limit ?? null, params.page_token ?? null]);
396
+ const existing = this.peerCatalogQueries.get(key);
397
+ if (existing) {
398
+ existing.application ||= !background;
399
+ return await existing.promise;
400
+ }
401
+ const query = { application: !background, promise: Promise.resolve({}) };
402
+ query.promise = this.queryPeers(params, () => !query.application);
403
+ this.peerCatalogQueries.set(key, query);
404
+ try {
405
+ return await query.promise;
406
+ }
407
+ finally {
408
+ if (this.peerCatalogQueries.get(key) === query)
409
+ this.peerCatalogQueries.delete(key);
410
+ }
411
+ }
412
+ async queryPeers(params, isBackground) {
413
+ const client = this.runtime.client;
414
+ const context = this.ensurePeerContext();
415
+ const generation = this.captureInlineAckGeneration();
416
+ const aid = client._aid;
417
+ const deviceId = client._deviceId;
418
+ const slotId = client._slotId;
419
+ const filter = String(params.filter ?? 'pending');
420
+ if (filter !== 'pending' && filter !== 'all')
421
+ throw new Error('invalid peers filter');
422
+ if (filter === 'pending' && ('limit' in params || 'page_token' in params))
423
+ throw new Error('pending peers cannot be paginated');
424
+ const raw = await client._rpcPipeline.rawCall('message.peers', {
425
+ ...params, filter, device_id: deviceId, slot_id: slotId,
426
+ }, { background: isBackground() });
427
+ if (context !== this.ensurePeerContext() || generation !== this.captureInlineAckGeneration())
428
+ throw new Error('peer catalog invalidated');
429
+ if (!raw || raw.cursor_scope !== 'peer' || raw.catalog_ready !== true || !Array.isArray(raw.peers))
430
+ throw new Error('peer catalog is not ready');
431
+ if (filter === 'pending' && (raw.has_more === true || raw.next_page_token))
432
+ throw new Error('pending peer catalog is incomplete');
433
+ const baseline = nonNegativeSafeSequenceHint(raw.bootstrap_floor_seq);
434
+ if (baseline === null)
435
+ throw new Error('invalid peer bootstrap_floor_seq');
436
+ if (this.peerBootstrap !== null && this.peerBootstrap !== baseline)
437
+ throw new Error('peer bootstrap changed');
438
+ if (this.peerBootstrap === null) {
439
+ const store = client._tokenStore;
440
+ const existing = await store.loadInstanceState?.(aid, deviceId, slotId);
441
+ if (context !== this.ensurePeerContext() || generation !== this.captureInlineAckGeneration())
442
+ throw new Error('peer catalog invalidated');
443
+ const saved = existing?.p2p_peer_bootstrap_floor_seq;
444
+ if (saved !== undefined && saved !== baseline)
445
+ throw new Error('persisted peer bootstrap changed');
446
+ if (typeof store.updateInstanceState !== 'function')
447
+ throw new Error('peer bootstrap persistence is unavailable');
448
+ await store.updateInstanceState(aid, deviceId, slotId, (state) => ({ ...state, p2p_peer_bootstrap_floor_seq: baseline }));
449
+ if (context !== this.ensurePeerContext() || generation !== this.captureInlineAckGeneration())
450
+ throw new Error('peer catalog invalidated');
451
+ this.peerBootstrap = baseline;
452
+ }
453
+ const rows = [];
454
+ for (const entry of raw.peers) {
455
+ if (!entry || typeof entry.sender_aid !== 'string' || !entry.sender_aid.trim())
456
+ throw new Error('peer catalog sender is invalid');
457
+ await this.initializePeer(entry.sender_aid, nonNegativeSafeSequenceHint(entry.initial_after_seq) ?? baseline);
458
+ if (context !== this.ensurePeerContext() || generation !== this.captureInlineAckGeneration())
459
+ throw new Error('peer catalog invalidated');
460
+ rows.push({ ...entry });
461
+ }
462
+ for (const row of rows) {
463
+ const ns = this.peerNamespace(String(row.sender_aid));
464
+ const hint = nonNegativeSafeSequenceHint(row.next_pending_seq);
465
+ const confirmed = Math.max(this.peerPullConfirmedThrough.get(ns) ?? 0, nonNegativeSafeSequenceHint(row.server_ack_seq) ?? nonNegativeSafeSequenceHint(row.initial_after_seq) ?? 0);
466
+ this.peerPullConfirmedThrough.set(ns, confirmed);
467
+ const current = this.peerPullHints.get(ns);
468
+ if (current !== undefined && current <= confirmed)
469
+ this.peerPullHints.delete(ns);
470
+ if (hint !== null && hint >= confirmed)
471
+ this.peerPullHints.set(ns, Math.max(this.peerPullHints.get(ns) ?? 0, hint));
472
+ }
473
+ const snapshot = {
474
+ ...raw, filter, peers: rows, aid, device_id: deviceId, slot_id: slotId,
475
+ trigger: isBackground() ? 'background' : 'application', peer_count: rows.length,
476
+ };
477
+ if (!isBackground() || rows.length > 0)
478
+ client._dispatcher.enqueue('message.peers_snapshot', snapshot, () => context === this.ensurePeerContext() && generation === this.captureInlineAckGeneration());
479
+ return snapshot;
480
+ }
481
+ async pullPeers(params = {}, background = false) {
482
+ const client = this.runtime.client;
483
+ const sender = String(params.sender_aid ?? '').trim();
484
+ if (!sender && ('after_seq' in params || 'after_volatile_cursor' in params || 'window_mode' in params || params.force === true))
485
+ throw new Error('explicit peer is required for after_seq, after_volatile_cursor, window_mode or force');
486
+ const snapshot = sender ? null : await this.peers({}, background);
487
+ const rows = sender ? [{ sender_aid: sender }] : snapshot.peers;
488
+ const budget = { remaining: Math.max(1, Math.trunc(Number(params.max_pages ?? 100) || 100)) };
489
+ const results = await Promise.all(rows.map(async (row) => {
490
+ const peer = String(row.sender_aid);
491
+ const ns = await this.initializePeer(peer);
492
+ try {
493
+ const state = this.syncState(ns);
494
+ if (!params.window_mode && params.force !== true && this.peerOwnType() === 'human' && state.head <= state.ack && budget.remaining > 0) {
495
+ const tail = await client._v2E2EE.pullPeerV2(peer, { ...params, window_mode: 'tail', budget, background });
496
+ if (!budget.remaining) {
497
+ if (row.volatile_has_more)
498
+ this.schedulePeerRecovery(peer, Number(tail.head_seq ?? 0), true);
499
+ return { ...tail, complete: tail.complete && !row.volatile_has_more };
500
+ }
501
+ const forward = await client._v2E2EE.pullPeerV2(peer, { ...params, budget, background });
502
+ const messages = [...new Map([...tail.messages, ...forward.messages].map((message) => [message.seq ?? message.volatile_cursor, message])).values()];
503
+ return { ...forward, messages };
504
+ }
505
+ return await client._v2E2EE.pullPeerV2(peer, { ...params, budget, background });
506
+ }
507
+ catch (error) {
508
+ this.schedulePeerRecovery(peer, Number(row.head_seq ?? 0), Boolean(row.volatile_has_more));
509
+ return { sender_aid: peer, messages: [], complete: false, ack_seq: this.syncState(ns).ack, error: error instanceof Error ? error.message : String(error) };
510
+ }
511
+ }));
512
+ return { cursor_scope: 'peer', peers: results, messages: results.flatMap((row) => row.messages), partial: results.some((row) => !row.complete) };
513
+ }
514
+ refreshPendingPeers() {
515
+ const client = this.runtime.client;
516
+ if (client._sessionOptions?.background_sync === false || !client._aid || client._closing || client.state !== ConnectionState.READY)
517
+ return;
518
+ this.ensurePeerContext();
519
+ this.peerCatalogDirty = true;
520
+ if (this.peerCatalogTask)
521
+ return;
522
+ const generation = this.captureInlineAckGeneration();
523
+ const task = (async () => {
524
+ while (this.peerCatalogDirty && generation === this.captureInlineAckGeneration()) {
525
+ this.peerCatalogDirty = false;
526
+ const result = await this.peers({}, true);
527
+ const rows = [...result.peers];
528
+ const hint = (row) => this.peerPullPriority(this.peerNamespace(String(row.sender_aid)));
529
+ rows.sort((left, right) => hint(left) - hint(right));
530
+ for (const row of rows)
531
+ this.schedulePeerRecovery(String(row.sender_aid), Number(row.head_seq ?? 0), Boolean(row.volatile_has_more));
532
+ }
533
+ })();
534
+ this.peerCatalogTask = task.then(() => ({}), () => ({}));
535
+ client._safeAsync(task.catch(() => {
536
+ if (generation !== this.captureInlineAckGeneration())
537
+ return;
538
+ const timer = setTimeout(() => { if (generation === this.captureInlineAckGeneration())
539
+ this.refreshPendingPeers(); }, BACKGROUND_PULL_RETRY_DELAY_MS);
540
+ timer.unref?.();
541
+ }).finally(() => { if (generation === this.captureInlineAckGeneration())
542
+ this.peerCatalogTask = null; }));
543
+ }
544
+ schedulePeerRecovery(sender, head = 0, volatile = false) {
545
+ const client = this.runtime.client;
546
+ if (client._sessionOptions?.background_sync === false || !sender || client._closing)
547
+ return;
548
+ this.ensurePeerContext();
549
+ const need = this.peerRecoveryNeeds.get(sender) ?? { head: 0, volatile: false, pending: false };
550
+ need.head = Math.max(need.head, head);
551
+ need.volatile ||= volatile;
552
+ need.pending = true;
553
+ this.peerRecoveryNeeds.set(sender, need);
554
+ if (this.peerRecoveryTasks.has(sender))
555
+ return;
556
+ const generation = this.captureInlineAckGeneration();
557
+ const task = (async () => {
558
+ await this.initializePeer(sender);
559
+ const state = this.syncState(this.peerNamespace(sender));
560
+ if (this.peerOwnType() === 'human' && need.head > state.head && state.ack >= state.tail - 1) {
561
+ await client._v2E2EE.pullPeerV2(sender, { window_mode: 'tail', background: true, retainMessages: false, budget: { remaining: 1 } });
562
+ }
563
+ while (generation === this.captureInlineAckGeneration() && this.peerRecoveryNeeds.has(sender)) {
564
+ need.pending = false;
565
+ const result = await client._v2E2EE.pullPeerV2(sender, { background: true, retainMessages: false, budget: { remaining: 100 } });
566
+ if (result.complete && result.ack_seq >= need.head && !need.pending) {
567
+ this.peerRecoveryNeeds.delete(sender);
568
+ break;
569
+ }
570
+ await new Promise((resolve) => setTimeout(resolve, BACKGROUND_PULL_RETRY_DELAY_MS));
571
+ }
572
+ })();
573
+ this.peerRecoveryTasks.set(sender, task);
574
+ client._safeAsync(task.catch(() => {
575
+ if (generation !== this.captureInlineAckGeneration())
576
+ return;
577
+ const timer = setTimeout(() => { if (generation === this.captureInlineAckGeneration())
578
+ this.schedulePeerRecovery(sender, need.head, need.volatile); }, BACKGROUND_PULL_RETRY_DELAY_MS);
579
+ timer.unref?.();
580
+ }).finally(() => {
581
+ if (generation === this.captureInlineAckGeneration()) {
582
+ this.peerRecoveryTasks.delete(sender);
583
+ if (this.peerCatalogDirty && this.peerRecoveryTasks.size === 0)
584
+ this.refreshPendingPeers();
585
+ }
586
+ }));
587
+ }
106
588
  isPullOperationCurrent(namespace = '') {
107
589
  const pipeline = this.runtime.client._rpcPipeline;
108
590
  return typeof pipeline?.isPullNamespaceCurrent === 'function'
@@ -170,15 +652,13 @@ export class MessageDeliveryEngine {
170
652
  const client = this.runtime.client;
171
653
  const response = result;
172
654
  const volatileMessages = method === 'message.pull' ? this.volatileMessages(response.messages) : [];
173
- if (volatileMessages.length > 0) {
174
- await this.publishPlainVolatileMessages(volatileMessages, 'pull', true);
175
- }
176
655
  this.ensurePullOperationCurrent(ns);
177
656
  this.syncStarted(ns);
178
657
  const messages = deduplicatePlainForwardPageMessages(response.messages, afterSeq);
179
658
  const coordinator = this.forwardCoordinator();
180
659
  const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
181
660
  const pageInlineGeneration = this.captureInlineAckGeneration();
661
+ const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
182
662
  const deferredServerCursor = Number(coordinator.pendingForwardCursor(ns, pageContigBefore) ?? 0);
183
663
  const deferredForwardAck = Number(coordinator.pendingForwardAck(ns, pageContigBefore) ?? 0);
184
664
  let pageTrackerSnapshot = null;
@@ -202,44 +682,51 @@ export class MessageDeliveryEngine {
202
682
  let publishedCount = 0;
203
683
  let committed = false;
204
684
  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
- }
685
+ await this.runApplicationPage(ns, 'pull', async () => {
686
+ if (volatileMessages.length > 0)
687
+ await this.publishPlainVolatileMessages(volatileMessages, 'pull', true);
688
+ for (const rawMessage of messages) {
213
689
  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')) {
690
+ const seq = positiveSafeSequenceHint(rawMessage.seq);
691
+ if (method === 'message.pull') {
692
+ const appEvent = this.p2pAppEventForMessage(rawMessage);
693
+ if (await this.publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false)) {
694
+ publishedCount += 1;
695
+ }
219
696
  this.ensurePullOperationCurrent(ns);
697
+ continue;
698
+ }
699
+ const message = normalizeGroupMentionMode(rawMessage);
700
+ if (this.recallEventFromGroupMessage(message)) {
701
+ if (await this.publishGroupRecallTombstone(groupId, seq, message, 'pull')) {
702
+ this.ensurePullOperationCurrent(ns);
703
+ this.markPublishedSeq(ns, seq);
704
+ publishedCount += 1;
705
+ }
706
+ }
707
+ else if (this.isSelfSentGroupMessage(message)) {
220
708
  this.markPublishedSeq(ns, seq);
709
+ }
710
+ else if (await this.publishPulledMessage('group.message_created', ns, seq, message, false)) {
711
+ this.ensurePullOperationCurrent(ns);
221
712
  publishedCount += 1;
222
713
  }
223
714
  }
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
- }
715
+ this.ensurePullOperationCurrent(ns);
716
+ if (pageInlineGeneration !== this.captureInlineAckGeneration())
717
+ throw new Error('pull invalidated');
718
+ const previousWindow = client._seqTracker.getSyncState(ns);
719
+ const scanTarget = Math.max(pageContigBefore, ...messages.map((message) => positiveSafeSequenceHint(message.seq)), positiveSafeSequenceHint(response.contiguous_seq), retentionFloor, visibilityFloor, deferredServerCursor);
720
+ const deliveryTarget = scanTarget >= previousWindow.tail - 1
721
+ ? Math.max(scanTarget, previousWindow.head) : scanTarget;
722
+ await this.drainOrderedMessages(ns, undefined, false, false, 'pull', undefined, deliveryTarget);
723
+ });
232
724
  this.ensurePullOperationCurrent(ns);
233
725
  if (pageInlineGeneration !== this.captureInlineAckGeneration())
234
726
  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())
727
+ if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
242
728
  throw new Error('pull invalidated');
729
+ this.rememberCommittedSeqState(ns);
243
730
  pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
244
731
  trackerMutated = true;
245
732
  client._seqTracker.onPullResult(ns, messages, afterSeq, Number.isSafeInteger(result.contiguous_seq)
@@ -255,11 +742,14 @@ export class MessageDeliveryEngine {
255
742
  this.ensurePullOperationCurrent(ns);
256
743
  if (pageInlineGeneration !== this.captureInlineAckGeneration())
257
744
  throw new Error('pull invalidated');
745
+ if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
746
+ throw new Error('pull invalidated');
258
747
  }
259
748
  committed = true;
260
749
  }
261
750
  catch (exc) {
262
751
  if (trackerMutated && pageInlineGeneration === this.captureInlineAckGeneration() && this.isPullOperationCurrent(ns)
752
+ && this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration)
263
753
  && !committed && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
264
754
  (client._seqTracker._restoreNamespaceSnapshotPreservingPush ?? client._seqTracker.restoreNamespaceSnapshot)
265
755
  .call(client._seqTracker, ns, pageTrackerSnapshot);
@@ -330,23 +820,39 @@ export class MessageDeliveryEngine {
330
820
  this.volatileCursor = '';
331
821
  this.volatileSeenMessages.clear();
332
822
  this.volatileInFlightMessages.clear();
823
+ for (const completion of this.volatileCompletions.values())
824
+ completion.reject(new Error('delivery invalidated'));
825
+ this.volatileCompletions.clear();
333
826
  }
334
827
  volatilePullCursor() {
335
828
  this.ensureVolatileContext();
336
829
  return this.volatileCursor;
337
830
  }
831
+ peerVolatilePullCursor(sender) {
832
+ this.ensurePeerContext();
833
+ return this.peerVolatileCursors.get(sender) ?? '';
834
+ }
835
+ setPeerVolatilePullCursor(sender, cursor) {
836
+ this.ensurePeerContext();
837
+ if (cursor)
838
+ this.peerVolatileCursors.set(sender, cursor);
839
+ else
840
+ this.peerVolatileCursors.delete(sender);
841
+ }
338
842
  resetVolatilePullCursor() {
339
843
  this.ensureVolatileContext();
340
844
  this.volatileCursor = '';
341
845
  }
342
846
  volatileSeenKey(message) {
343
847
  const messageId = String(message.message_id ?? '').trim();
344
- const sender = String(message.from_aid ?? message.from ?? '').trim();
848
+ const sender = String(message.sender_aid ?? message.from_aid ?? message.from ?? '').trim();
345
849
  const cursor = String(message.volatile_cursor ?? '').trim();
346
- return `${sender}|${messageId || cursor}`;
850
+ return JSON.stringify([sender, messageId, cursor]);
347
851
  }
348
852
  isVolatileSeen(message) {
349
853
  this.ensureVolatileContext();
854
+ if (this.currentApplicationPage()?.volatileSeen.has(this.volatileSeenKey(message)))
855
+ return true;
350
856
  const now = Date.now();
351
857
  for (const [key, expiresAt] of this.volatileSeenMessages) {
352
858
  if (expiresAt <= now)
@@ -356,6 +862,12 @@ export class MessageDeliveryEngine {
356
862
  }
357
863
  markVolatileSeen(message) {
358
864
  this.ensureVolatileContext();
865
+ const page = this.currentApplicationPage();
866
+ if (page) {
867
+ page.volatileSeen.add(this.volatileSeenKey(message));
868
+ page.commits.push(() => this.markVolatileSeen(message));
869
+ return;
870
+ }
359
871
  const now = Date.now();
360
872
  const expiresAt = Math.max(now + 60_000, Number(message.expires_at ?? now + 1_800_000) || now + 1_800_000);
361
873
  this.volatileSeenMessages.set(this.volatileSeenKey(message), expiresAt);
@@ -373,11 +885,38 @@ export class MessageDeliveryEngine {
373
885
  if (this.volatileInFlightMessages.has(key))
374
886
  return 'inflight';
375
887
  this.volatileInFlightMessages.add(key);
888
+ let resolve, reject;
889
+ const promise = new Promise((done, fail) => { resolve = done; reject = fail; });
890
+ void promise.catch(() => { });
891
+ const page = this.currentApplicationPage();
892
+ const completion = { page, promise, resolve, reject };
893
+ this.volatileCompletions.set(key, completion);
894
+ if (page)
895
+ void page.completion.then(resolve, reject).finally(() => {
896
+ if (this.volatileCompletions.get(key) === completion) {
897
+ this.volatileCompletions.delete(key);
898
+ this.volatileInFlightMessages.delete(key);
899
+ }
900
+ });
376
901
  return 'claimed';
377
902
  }
903
+ async waitVolatileDelivery(message) {
904
+ const completion = this.volatileCompletions.get(this.volatileSeenKey(message));
905
+ if (!completion || completion.page === this.currentApplicationPage())
906
+ return;
907
+ if (this.runtime.client._dispatcher._isApplicationHandler?.())
908
+ throw new Error('delivery cannot wait inside an application handler');
909
+ await completion.promise;
910
+ }
378
911
  finishVolatileDelivery(message) {
379
912
  this.ensureVolatileContext();
380
- this.volatileInFlightMessages.delete(this.volatileSeenKey(message));
913
+ const key = this.volatileSeenKey(message);
914
+ const completion = this.volatileCompletions.get(key);
915
+ if (completion?.page)
916
+ return;
917
+ completion?.resolve();
918
+ this.volatileCompletions.delete(key);
919
+ this.volatileInFlightMessages.delete(key);
381
920
  }
382
921
  publicVolatileMessage(message) {
383
922
  const result = this.stripInternalSenderDeviceFields(message);
@@ -385,15 +924,27 @@ export class MessageDeliveryEngine {
385
924
  return result;
386
925
  }
387
926
  async ackVolatileMessages(messages, v2, advanceCursor) {
927
+ const page = this.currentApplicationPage();
928
+ if (page) {
929
+ page.afterDelivery.push(() => this.ackVolatileMessages(messages, v2, advanceCursor));
930
+ return undefined;
931
+ }
388
932
  const cursors = [...new Set(messages
389
933
  .map((message) => String(message.volatile_cursor ?? '').trim())
390
934
  .filter(Boolean))];
391
935
  if (cursors.length === 0)
392
936
  return undefined;
393
- const result = await this.runtime.client._callRawV2Rpc(v2 ? 'message.v2.ack' : 'message.ack', { volatile_cursors: cursors, _rpc_background: true });
937
+ const client = this.runtime.client;
938
+ const sender = client._p2pPeerPullEnabled ? String(messages[0]?.sender_aid ?? messages[0]?.from_aid ?? messages[0]?.from ?? '') : '';
939
+ const result = sender
940
+ ? await client._v2E2EE.ackPeerV2(sender, 0, cursors)
941
+ : await client._callRawV2Rpc(v2 ? 'message.v2.ack' : 'message.ack', { volatile_cursors: cursors, _rpc_background: true });
394
942
  if (advanceCursor) {
395
943
  this.ensureVolatileContext();
396
- this.volatileCursor = cursors[cursors.length - 1] ?? '';
944
+ if (sender)
945
+ this.setPeerVolatilePullCursor(sender, cursors[cursors.length - 1] ?? '');
946
+ else
947
+ this.volatileCursor = cursors[cursors.length - 1] ?? '';
397
948
  }
398
949
  return result;
399
950
  }
@@ -406,8 +957,12 @@ export class MessageDeliveryEngine {
406
957
  processed.push(rawMessage);
407
958
  continue;
408
959
  }
409
- if (claim === 'inflight')
960
+ if (claim === 'inflight') {
961
+ await this.waitVolatileDelivery(message);
962
+ if (this.isVolatileSeen(message))
963
+ processed.push(rawMessage);
410
964
  continue;
965
+ }
411
966
  try {
412
967
  const appEvent = this.p2pAppEventForMessage(message);
413
968
  await this.runtime.client._publishAppEvent(appEvent.event, appEvent.payload, source);
@@ -445,6 +1000,12 @@ export class MessageDeliveryEngine {
445
1000
  this.onlineUnreadHintTasks = null;
446
1001
  this.realtimeTailHeads = null;
447
1002
  this.syncSessions = null;
1003
+ this.peerCatalogTask = null;
1004
+ this.peerCatalogDirty = false;
1005
+ this.peerRecoveryNeeds.clear();
1006
+ this.peerPullHints.clear();
1007
+ this.peerPullConfirmedThrough.clear();
1008
+ this.peerRecoveryTasks.clear();
448
1009
  }
449
1010
  deliveryChangedNamespace(event, payload) {
450
1011
  if (event === 'group.message_created' && isJsonObject(payload)) {
@@ -716,7 +1277,8 @@ export class MessageDeliveryEngine {
716
1277
  return this.pendingGroupPullUpper?.get(ns) ?? 0;
717
1278
  }
718
1279
  hasAnyPendingPull() {
719
- return [...(this.pendingGroupPullUpper?.values() ?? [])].some((seq) => seq > 0)
1280
+ return this.peerCatalogDirty || this.peerRecoveryNeeds.size > 0 || this.peerRecoveryTasks.size > 0 || Boolean(this.peerCatalogTask)
1281
+ || [...(this.pendingGroupPullUpper?.values() ?? [])].some((seq) => seq > 0)
720
1282
  || this.backgroundPullNeeds.size > 0
721
1283
  || Boolean(this.onlineUnreadHintTasks?.size)
722
1284
  || Boolean(this.realtimeSyncing?.size)
@@ -779,7 +1341,7 @@ export class MessageDeliveryEngine {
779
1341
  this.requestBackgroundPullRecovery();
780
1342
  }
781
1343
  requestBackgroundPullRecovery() {
782
- if (this.backgroundPullCheckQueued || this.backgroundPullNeeds.size === 0)
1344
+ if (this.backgroundPullCheckQueued || (this.backgroundPullNeeds.size === 0 && !this.peerCatalogDirty && this.peerRecoveryNeeds.size === 0))
783
1345
  return;
784
1346
  this.backgroundPullCheckQueued = true;
785
1347
  const generation = this.captureInlineAckGeneration();
@@ -790,6 +1352,14 @@ export class MessageDeliveryEngine {
790
1352
  const client = this.runtime.client;
791
1353
  if (client._sessionOptions?.background_sync === false || client.state !== ConnectionState.READY || client._closing)
792
1354
  return;
1355
+ if (client._p2pPeerPullEnabled) {
1356
+ if (this.peerCatalogDirty && !this.peerCatalogTask)
1357
+ this.refreshPendingPeers();
1358
+ for (const [sender, need] of this.peerRecoveryNeeds) {
1359
+ if (!this.peerRecoveryTasks.has(sender))
1360
+ this.schedulePeerRecovery(sender, need.head, need.volatile);
1361
+ }
1362
+ }
793
1363
  let retryAt = Infinity;
794
1364
  for (const [ns, need] of this.backgroundPullNeeds) {
795
1365
  if (this.backgroundPullTasks.has(ns) || this.realtimeSyncing?.has(ns) || this.onlineUnreadHintTasks?.has(ns))
@@ -845,6 +1415,8 @@ export class MessageDeliveryEngine {
845
1415
  }
846
1416
  onForegroundPullsDrained() {
847
1417
  const client = this.runtime.client;
1418
+ if (client._p2pPeerPullEnabled)
1419
+ return;
848
1420
  if (!client._aid || client._sessionOptions?.background_sync === false
849
1421
  || client.state !== ConnectionState.READY || client._closing)
850
1422
  return;
@@ -898,6 +1470,12 @@ export class MessageDeliveryEngine {
898
1470
  }
899
1471
  }
900
1472
  markPublishedSeq(ns, seq) {
1473
+ const page = this.currentApplicationPage();
1474
+ if (page) {
1475
+ page.consumed.add(JSON.stringify(['published', ns, seq]));
1476
+ page.commits.push(() => this.markPublishedSeq(ns, seq));
1477
+ return;
1478
+ }
901
1479
  const client = this.runtime.client;
902
1480
  let pushed = client._pushedSeqs.get(ns);
903
1481
  if (!pushed) {
@@ -910,6 +1488,18 @@ export class MessageDeliveryEngine {
910
1488
  client._pushedSeqs.set(ns, new Set(keep));
911
1489
  }
912
1490
  }
1491
+ hasPublishedSeq(ns, seq) {
1492
+ return this.runtime.client._pushedSeqs.get(ns)?.has(seq) === true
1493
+ || this.currentApplicationPage()?.consumed.has(JSON.stringify(['published', ns, seq])) === true;
1494
+ }
1495
+ consumeOrderedMessage(ns, seq, queue) {
1496
+ this.currentApplicationPage()?.consumed.add(JSON.stringify(['ordered', ns, seq]));
1497
+ this.completeApplicationDelivery(() => {
1498
+ queue?.delete(seq);
1499
+ if (queue?.size === 0)
1500
+ this.runtime.client._pendingOrderedMsgs.delete(ns);
1501
+ });
1502
+ }
913
1503
  enqueueOrderedMessage(ns, event, seq, payload, source = 'push') {
914
1504
  const client = this.runtime.client;
915
1505
  let queue = client._pendingOrderedMsgs.get(ns);
@@ -960,21 +1550,38 @@ export class MessageDeliveryEngine {
960
1550
  async publishOrderedGroupChanged(payload, source = 'ordered') {
961
1551
  const client = this.runtime.client;
962
1552
  const generation = this.captureInlineAckGeneration();
1553
+ const scope = this.applicationPage.getStore();
1554
+ const page = scope?.collecting ? scope : undefined;
1555
+ const owner = this.applicationClaims.get(this.applicationClaimKey('group.changed', payload, this.applicationNamespace('group.changed', payload), this.ensurePeerContext()));
1556
+ if (owner) {
1557
+ if (owner !== this.currentApplicationPage())
1558
+ await this.waitForApplicationOwner(owner);
1559
+ if (generation !== this.captureInlineAckGeneration())
1560
+ throw new Error('pull invalidated');
1561
+ return;
1562
+ }
963
1563
  const published = client._publishAppEvent('group.changed', payload, source);
964
1564
  if (isPromiseLike(published))
965
1565
  await published;
966
1566
  if (generation !== this.captureInlineAckGeneration())
967
1567
  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);
1568
+ this.completeApplicationDelivery(() => {
1569
+ if (isJsonObject(payload)) {
1570
+ const eventPayload = payload;
1571
+ client._groupState?.handleGroupChangedV2Membership?.(eventPayload);
1572
+ if (eventPayload.action === 'dissolved') {
1573
+ const rawGroupId = eventPayload.group_id ?? eventPayload.group_aid ?? '';
1574
+ const groupId = String(rawGroupId).trim();
1575
+ if (groupId) {
1576
+ const cleanup = () => { client._cleanupDissolvedGroup?.(groupId); };
1577
+ if (page)
1578
+ page.cleanup.push(cleanup);
1579
+ else
1580
+ cleanup();
1581
+ }
1582
+ }
976
1583
  }
977
- }
1584
+ });
978
1585
  }
979
1586
  isInstanceScopedMessageEvent(event) {
980
1587
  return event === 'message.received'
@@ -1078,6 +1685,7 @@ export class MessageDeliveryEngine {
1078
1685
  setIfPresent('timestamp', firstValue(message.timestamp, message.created_at, message.t_server));
1079
1686
  if ('encrypted' in message)
1080
1687
  envelope.encrypted = Boolean(message.encrypted);
1688
+ envelope.need_render = message.need_render !== false;
1081
1689
  const context = this.envelopeMetadata(message.context);
1082
1690
  if (context)
1083
1691
  envelope.context = context;
@@ -1145,6 +1753,7 @@ export class MessageDeliveryEngine {
1145
1753
  setIfPresent('version', firstValue(body.version, params.version));
1146
1754
  setIfPresent('timestamp', firstValue(params.timestamp, resultObj.timestamp, resultObj.created_at, resultObj.t_server, Date.now()));
1147
1755
  envelope.encrypted = Boolean(encrypted);
1756
+ envelope.need_render = params.need_render !== false;
1148
1757
  const context = this.envelopeMetadata(params.context);
1149
1758
  if (context)
1150
1759
  envelope.context = context;
@@ -1306,12 +1915,14 @@ export class MessageDeliveryEngine {
1306
1915
  throw new Error('pull invalidated');
1307
1916
  if (this.isPullScopedSource(source))
1308
1917
  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
- }
1918
+ this.completeApplicationDelivery(() => {
1919
+ seen.set(dedupKey, Date.now());
1920
+ if (seen.size > MESSAGE_RECALL_SEEN_LIMIT) {
1921
+ const drop = [...seen.entries()].sort((a, b) => a[1] - b[1]).slice(0, seen.size - MESSAGE_RECALL_SEEN_LIMIT);
1922
+ for (const [oldKey] of drop)
1923
+ seen.delete(oldKey);
1924
+ }
1925
+ });
1315
1926
  client._clientLog.debug(`message.recalled published: seq=${String(seq)} ids=${JSON.stringify(eventPayload.message_ids)}`);
1316
1927
  return true;
1317
1928
  }
@@ -1417,12 +2028,14 @@ export class MessageDeliveryEngine {
1417
2028
  throw new Error('pull invalidated');
1418
2029
  if (this.isPullScopedSource(source))
1419
2030
  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
- }
2031
+ this.completeApplicationDelivery(() => {
2032
+ seen.set(dedupKey, Date.now());
2033
+ if (seen.size > GROUP_RECALL_SEEN_LIMIT) {
2034
+ const drop = [...seen.entries()].sort((a, b) => a[1] - b[1]).slice(0, seen.size - GROUP_RECALL_SEEN_LIMIT);
2035
+ for (const [oldKey] of drop)
2036
+ seen.delete(oldKey);
2037
+ }
2038
+ });
1426
2039
  client._clientLog.debug(`group.message_recalled published: group=${groupId} seq=${String(seq)} ids=${JSON.stringify(eventPayload.message_ids)}`);
1427
2040
  return true;
1428
2041
  }
@@ -1471,6 +2084,14 @@ export class MessageDeliveryEngine {
1471
2084
  }
1472
2085
  publishAppEvent(event, payload, source = 'direct', deliveryContext) {
1473
2086
  const client = this.runtime.client;
2087
+ const scope = this.applicationPage.getStore();
2088
+ const page = scope?.collecting ? scope : undefined;
2089
+ if (APPLICATION_DELIVERY_EVENTS.has(event) && typeof client._dispatcher._snapshotApplicationHandlers === 'function') {
2090
+ const namespace = this.applicationNamespace(event, payload);
2091
+ if (!page || (page.namespace !== namespace && !(page.namespace.startsWith('p2p:') && event.startsWith('message.')))) {
2092
+ return this.runApplicationPage(namespace, source, async () => { await this.publishAppEvent(event, payload, source, deliveryContext); });
2093
+ }
2094
+ }
1474
2095
  if ((event === 'message.received' || event === 'group.message_created') && isJsonObject(payload)) {
1475
2096
  client._maybeAppendEchoTraceReceive(payload);
1476
2097
  }
@@ -1491,22 +2112,33 @@ export class MessageDeliveryEngine {
1491
2112
  }
1492
2113
  const generation = this.deliveryChangedGeneration;
1493
2114
  const normalized = this.normalizePublishedMessagePayload(event, payload, source);
2115
+ const note = () => {
2116
+ if (generation === this.deliveryChangedGeneration) {
2117
+ this.noteDeliveryChanged(event, payload, source, deliveryContext);
2118
+ }
2119
+ };
2120
+ if (page && APPLICATION_DELIVERY_EVENTS.has(event)) {
2121
+ if (page.batching)
2122
+ return this.collectApplicationEvent(page, event, normalized, note);
2123
+ const owner = this.applicationClaims.get(this.applicationClaimKey(event, normalized, page.namespace, page.context));
2124
+ if (owner)
2125
+ return this.waitForApplicationOwner(owner).then(() => this.ensureApplicationPageCurrent(page));
2126
+ }
1494
2127
  const dispatcher = client._dispatcher;
1495
2128
  const result = ['message.received', 'group.message_created', 'message.recalled', 'group.message_recalled', 'group.changed'].includes(event)
1496
2129
  && typeof dispatcher._publishAndWait === 'function'
1497
- ? dispatcher._publishAndWait(event, normalized)
2130
+ ? dispatcher._publishAndWait(event, normalized, undefined, page?.reservation, page ? () => this.isApplicationPageCurrent(page) : undefined)
1498
2131
  : typeof dispatcher.enqueue === 'function'
1499
2132
  ? (dispatcher.enqueue(event, normalized), undefined)
1500
2133
  : typeof dispatcher.publishSyncAware === 'function'
1501
2134
  ? dispatcher.publishSyncAware(event, normalized)
1502
2135
  : dispatcher.publish?.(event, normalized);
1503
- const note = () => {
1504
- if (generation === this.deliveryChangedGeneration) {
1505
- this.noteDeliveryChanged(event, payload, source, deliveryContext);
1506
- }
1507
- };
1508
2136
  if (isPromiseLike(result))
1509
- return Promise.resolve(result).then(note);
2137
+ return Promise.resolve(result).then(() => {
2138
+ if (page && APPLICATION_DELIVERY_EVENTS.has(event))
2139
+ this.ensureApplicationPageCurrent(page);
2140
+ note();
2141
+ });
1510
2142
  note();
1511
2143
  return result;
1512
2144
  }
@@ -1703,6 +2335,10 @@ export class MessageDeliveryEngine {
1703
2335
  }
1704
2336
  async onRawMessageReceived(data) {
1705
2337
  const client = this.runtime.client;
2338
+ if (client._p2pPeerPullEnabled) {
2339
+ await this.onPeerPush(data);
2340
+ return;
2341
+ }
1706
2342
  const tStart = Date.now();
1707
2343
  if (isJsonObject(data)) {
1708
2344
  client._logMessageDebug('server-push', '_raw.message.received', 'message.received', data);
@@ -1745,6 +2381,11 @@ export class MessageDeliveryEngine {
1745
2381
  if (!this.messageTargetsCurrentInstance(msg))
1746
2382
  return;
1747
2383
  const seq = msg.seq;
2384
+ if (client._p2pPeerPullEnabled) {
2385
+ await this.onPeerPush(msg);
2386
+ await this.publishMessageRecallTombstone(seq, msg);
2387
+ return;
2388
+ }
1748
2389
  if (seq !== undefined && seq !== null && client._aid) {
1749
2390
  const ns = `p2p:${client._aid}`;
1750
2391
  const seqNum = Number(seq);
@@ -1933,13 +2574,13 @@ export class MessageDeliveryEngine {
1933
2574
  const afterSeq = client._seqTracker.getContiguousSeq(ns);
1934
2575
  client._clientLog.debug(`auto pull group messages start: group=${groupId}, after_seq=${afterSeq}, seq=${String(notification.seq ?? '')}`);
1935
2576
  const operation = async () => {
1936
- const messages = await client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true });
2577
+ const messages = await client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true, background: true });
1937
2578
  this.prunePushedSeqs(ns);
1938
2579
  return messages.length;
1939
2580
  };
1940
2581
  if (client._rpcPipeline) {
1941
2582
  const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 });
1942
- await client._rpcPipeline.runPullSerialized(key, operation, true);
2583
+ await client._rpcPipeline.runPullNamespaceSerialized(key, operation, true);
1943
2584
  }
1944
2585
  else {
1945
2586
  await client._withBackgroundRpc(operation);
@@ -1947,6 +2588,10 @@ export class MessageDeliveryEngine {
1947
2588
  }
1948
2589
  async fillP2pGap() {
1949
2590
  const client = this.runtime.client;
2591
+ if (client._p2pPeerPullEnabled) {
2592
+ this.refreshPendingPeers();
2593
+ return;
2594
+ }
1950
2595
  if (client._sessionOptions?.background_sync === false)
1951
2596
  return;
1952
2597
  if (!client._aid)
@@ -2004,9 +2649,9 @@ export class MessageDeliveryEngine {
2004
2649
  let filled = 0;
2005
2650
  let failed = false;
2006
2651
  try {
2007
- const operation = () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true });
2652
+ const operation = () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true, background: true });
2008
2653
  const messages = client._rpcPipeline
2009
- ? await client._rpcPipeline.runPullSerialized(client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 }), operation, true)
2654
+ ? await client._rpcPipeline.runPullNamespaceSerialized(client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 }), operation, true)
2010
2655
  : await client._withBackgroundRpc(operation);
2011
2656
  filled = messages.length;
2012
2657
  this.prunePushedSeqs(ns);
@@ -2120,6 +2765,7 @@ export class MessageDeliveryEngine {
2120
2765
  if (!Array.isArray(events))
2121
2766
  return;
2122
2767
  const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
2768
+ const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
2123
2769
  let pageTrackerSnapshot = null;
2124
2770
  let trackerMutated = false;
2125
2771
  const eventObjects = events.filter((evt) => isJsonObject(evt));
@@ -2135,35 +2781,37 @@ export class MessageDeliveryEngine {
2135
2781
  const eventSeqs = [];
2136
2782
  let hasDissolvedEvent = false;
2137
2783
  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);
2784
+ try {
2785
+ await this.runApplicationPage(ns, 'pull', async () => {
2786
+ for (const evt of eventObjects) {
2787
+ const eventSeq = Number(evt.event_seq ?? 0);
2788
+ if (Number.isFinite(eventSeq) && eventSeq > 0)
2789
+ eventSeqs.push(eventSeq);
2790
+ evt._from_gap_fill = true;
2791
+ if (evt.action === 'dissolved')
2792
+ hasDissolvedEvent = true;
2793
+ const et = String(evt.event_type ?? '');
2794
+ if (et !== 'group.message_created') {
2795
+ const cs = evt.client_signature;
2796
+ if (cs && isJsonObject(cs)) {
2797
+ if (client._shouldSkipEventSignature(evt)) {
2798
+ delete evt.client_signature;
2799
+ }
2800
+ else {
2801
+ evt._verified = await client._verifyEventSignatureAsync(evt, cs);
2802
+ }
2803
+ }
2804
+ if (Number.isFinite(eventSeq) && eventSeq > 0 && !client._pushedSeqs.get(ns)?.has(eventSeq)) {
2805
+ this.enqueueOrderedMessage(ns, 'group.changed', eventSeq, evt, 'pull');
2806
+ publishedEventCount += 1;
2807
+ }
2154
2808
  }
2809
+ if (et !== 'group.message_created')
2810
+ filled += 1;
2155
2811
  }
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);
2812
+ const scanTarget = Math.max(pageContigBefore, effectiveFloor, positiveSafeSequenceHint(result.contiguous_seq), ...eventSeqs);
2813
+ await this.drainOrderedMessages(ns, undefined, false, false, 'pull', undefined, scanTarget);
2814
+ });
2167
2815
  if (generation !== this.captureInlineAckGeneration())
2168
2816
  throw new Error('pull invalidated');
2169
2817
  this.ensurePullOperationCurrent(ns);
@@ -2171,6 +2819,9 @@ export class MessageDeliveryEngine {
2171
2819
  dissolved = true;
2172
2820
  break;
2173
2821
  }
2822
+ if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
2823
+ throw new Error('pull invalidated');
2824
+ this.rememberCommittedSeqState(ns);
2174
2825
  pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
2175
2826
  trackerMutated = true;
2176
2827
  client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq, Number.isSafeInteger(result.contiguous_seq)
@@ -2183,10 +2834,13 @@ export class MessageDeliveryEngine {
2183
2834
  this.ensurePullOperationCurrent(ns);
2184
2835
  if (generation !== this.captureInlineAckGeneration())
2185
2836
  throw new Error('pull invalidated');
2837
+ if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
2838
+ throw new Error('pull invalidated');
2186
2839
  }
2187
2840
  }
2188
2841
  catch (exc) {
2189
2842
  if (trackerMutated && generation === this.captureInlineAckGeneration() && this.isPullOperationCurrent(ns)
2843
+ && this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration)
2190
2844
  && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
2191
2845
  (client._seqTracker._restoreNamespaceSnapshotPreservingPush ?? client._seqTracker.restoreNamespaceSnapshot)
2192
2846
  .call(client._seqTracker, ns, pageTrackerSnapshot);
@@ -2665,11 +3319,11 @@ export class MessageDeliveryEngine {
2665
3319
  window_mode: 'tail',
2666
3320
  after_seq: after,
2667
3321
  limit: pageLimit,
2668
- }, true);
3322
+ }, true, true);
2669
3323
  tailHead = this.syncState(ns).head;
2670
3324
  return tailResult;
2671
3325
  }
2672
- const opts = { gateLocked: true };
3326
+ const opts = { gateLocked: true, background };
2673
3327
  if (request.max_pages !== undefined)
2674
3328
  opts.maxPages = request.max_pages;
2675
3329
  const messages = await client._pullGroupV2(groupId, after, pageLimit, opts);
@@ -2678,7 +3332,7 @@ export class MessageDeliveryEngine {
2678
3332
  if (!client._rpcPipeline)
2679
3333
  return await invoke();
2680
3334
  const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, ...request });
2681
- return await client._rpcPipeline.runPullSerialized(key, invoke, background);
3335
+ return await client._rpcPipeline.runPullNamespaceSerialized(key, invoke, background);
2682
3336
  };
2683
3337
  if (order.length === 1 && order[0] === 'tail') {
2684
3338
  result = await run({ window_mode: 'tail', after_seq: this.syncState(ns).ack, limit: pageLimit }, false);
@@ -2729,13 +3383,13 @@ export class MessageDeliveryEngine {
2729
3383
  const maxPages = Math.max(1, Math.ceil(Math.max(0, throughSeq - ack) / pageLimit));
2730
3384
  const request = { group_id: groupId, after_seq: ack, limit: pageLimit, max_pages: maxPages };
2731
3385
  const invoke = async () => {
2732
- const messages = await client._pullGroupV2(groupId, Number(request.after_seq), pageLimit, { gateLocked: true, maxPages });
3386
+ const messages = await client._pullGroupV2(groupId, Number(request.after_seq), pageLimit, { gateLocked: true, maxPages, background });
2733
3387
  return { messages, raw_count: messages.length };
2734
3388
  };
2735
3389
  if (!client._rpcPipeline)
2736
3390
  return await invoke();
2737
3391
  const key = client._rpcPipeline.pullGateKeyForCall('group.pull', request);
2738
- return await client._rpcPipeline.runPullSerialized(key, invoke, background);
3392
+ return await client._rpcPipeline.runPullNamespaceSerialized(key, invoke, background);
2739
3393
  }
2740
3394
  syncState(ns) {
2741
3395
  const tracker = this.runtime.client._seqTracker;
@@ -3076,6 +3730,7 @@ export class MessageDeliveryEngine {
3076
3730
  return false;
3077
3731
  const client = this.runtime.client;
3078
3732
  const tracker = client._seqTracker;
3733
+ const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
3079
3734
  let snapshot = null;
3080
3735
  let trackerMutated = false;
3081
3736
  let delivered = false;
@@ -3092,6 +3747,9 @@ export class MessageDeliveryEngine {
3092
3747
  delivered = true;
3093
3748
  if (!this.inlineGenerationIsCurrent(generation))
3094
3749
  return false;
3750
+ if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
3751
+ return false;
3752
+ this.rememberCommittedSeqState(ns);
3095
3753
  snapshot = typeof tracker.snapshotNamespace === 'function' ? tracker.snapshotNamespace(ns) : null;
3096
3754
  trackerMutated = true;
3097
3755
  const needsPull = Boolean(tracker.onMessageSeq(ns, seq));
@@ -3110,13 +3768,14 @@ export class MessageDeliveryEngine {
3110
3768
  if (!this.inlineGenerationIsCurrent(generation))
3111
3769
  return false;
3112
3770
  await client._commitSeqTrackerState(ns);
3113
- if (!this.inlineGenerationIsCurrent(generation))
3771
+ if (!this.inlineGenerationIsCurrent(generation) || !this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
3114
3772
  return false;
3115
3773
  this.markPublishedSeq(ns, seq);
3116
3774
  return true;
3117
3775
  }
3118
3776
  catch (exc) {
3119
3777
  if (trackerMutated && this.inlineGenerationIsCurrent(generation)
3778
+ && this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration)
3120
3779
  && typeof tracker.restoreNamespaceSnapshot === 'function') {
3121
3780
  (tracker._restoreNamespaceSnapshotPreservingPush ?? tracker.restoreNamespaceSnapshot).call(tracker, ns, snapshot);
3122
3781
  }
@@ -3131,6 +3790,8 @@ export class MessageDeliveryEngine {
3131
3790
  const tracker = client._seqTracker;
3132
3791
  if (!this.inlineGenerationIsCurrent(generation) || !this.inlineStateIsCaughtUp(ns, seq))
3133
3792
  return false;
3793
+ const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
3794
+ this.rememberCommittedSeqState(ns);
3134
3795
  const snapshot = typeof tracker.snapshotNamespace === 'function' ? tracker.snapshotNamespace(ns) : null;
3135
3796
  try {
3136
3797
  const needsPull = Boolean(tracker.onMessageSeq(ns, seq));
@@ -3145,10 +3806,11 @@ export class MessageDeliveryEngine {
3145
3806
  throw new Error('inline realtime terminal cursor did not collapse after consume');
3146
3807
  }
3147
3808
  await client._commitSeqTrackerState(ns);
3148
- return this.inlineGenerationIsCurrent(generation);
3809
+ return this.inlineGenerationIsCurrent(generation) && this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration);
3149
3810
  }
3150
3811
  catch (exc) {
3151
3812
  if (this.inlineGenerationIsCurrent(generation)
3813
+ && this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration)
3152
3814
  && typeof tracker.restoreNamespaceSnapshot === 'function') {
3153
3815
  (tracker._restoreNamespaceSnapshotPreservingPush ?? tracker.restoreNamespaceSnapshot).call(tracker, ns, snapshot);
3154
3816
  }
@@ -3342,8 +4004,83 @@ export class MessageDeliveryEngine {
3342
4004
  }
3343
4005
  return 0;
3344
4006
  }
4007
+ async onPeerPush(data, expectedVersion) {
4008
+ const client = this.runtime.client;
4009
+ if (!isJsonObject(data) || !client._aid || !this.messageTargetsCurrentInstance(data))
4010
+ return;
4011
+ const head = data;
4012
+ if (String(head.kind ?? '') === 'message.online_unread_hint' || !head.sender_aid) {
4013
+ this.refreshPendingPeers();
4014
+ return;
4015
+ }
4016
+ if (head.cursor_scope !== 'peer') {
4017
+ this.refreshPendingPeers();
4018
+ return;
4019
+ }
4020
+ const sender = String(head.sender_aid);
4021
+ const ns = await this.initializePeer(sender);
4022
+ const generation = this.captureInlineAckGeneration();
4023
+ const seq = positiveSafeSequenceHint(head.seq);
4024
+ if (this.isVolatileMessage(head)) {
4025
+ await client._v2E2EE.publishV2VolatileMessages([head], 'push', false);
4026
+ return;
4027
+ }
4028
+ if (!seq) {
4029
+ this.refreshPendingPeers();
4030
+ return;
4031
+ }
4032
+ client._seqTracker.updateMaxSeen(ns, seq);
4033
+ const row = isJsonObject(head.inline_message) ? head.inline_message : null;
4034
+ const prev = nonNegativeSafeSequenceHint(head.prev_peer_seq);
4035
+ if (row && prev !== null && prev < seq && prev <= this.syncState(ns).ack
4036
+ && (!expectedVersion || this.inlineRowKind(row) === expectedVersion) && this.inlineHeadMatches(head, row, seq)) {
4037
+ const consumed = await client._tryRunInlineRealtime(ns, seq, () => this.runPeerPageSerialized(ns, async () => {
4038
+ if (generation !== this.captureInlineAckGeneration() || prev > this.syncState(ns).ack)
4039
+ return false;
4040
+ const decoded = await this.decodeInlineP2P(row);
4041
+ if (!decoded || !this.decodedInlineMatches(head, decoded, seq))
4042
+ return false;
4043
+ decoded.sender_aid = sender;
4044
+ decoded.need_render = typeof row.need_render === 'boolean' ? row.need_render : decoded.need_render !== false;
4045
+ const event = this.p2pAppEventForMessage(decoded);
4046
+ await this.publishPulledMessage(event.event, ns, seq, event.payload, false, 'inline_push');
4047
+ if (generation !== this.captureInlineAckGeneration())
4048
+ throw new Error('inline invalidated');
4049
+ const snapshot = client._seqTracker.snapshotNamespace(ns);
4050
+ try {
4051
+ client._seqTracker.onPullResult(ns, [{ seq }], this.syncState(ns).ack, seq);
4052
+ await this.commitSeqTrackerState(ns);
4053
+ }
4054
+ catch (error) {
4055
+ client._seqTracker._restoreNamespaceSnapshotPreservingPush(ns, snapshot);
4056
+ throw error;
4057
+ }
4058
+ await client._v2E2EE.ackPeerV2(sender, this.syncState(ns).ack);
4059
+ return true;
4060
+ })).catch((error) => {
4061
+ if (generation === this.captureInlineAckGeneration())
4062
+ this.schedulePeerRecovery(sender, seq);
4063
+ throw error;
4064
+ });
4065
+ if (consumed?.acquired && consumed.value)
4066
+ return;
4067
+ }
4068
+ if (seq <= this.syncState(ns).ack)
4069
+ return;
4070
+ if (client._sessionOptions?.background_sync !== false) {
4071
+ this.schedulePeerRecovery(sender, seq);
4072
+ }
4073
+ else {
4074
+ await client._v2E2EE.pullPeerV2(sender, { limit: _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize, budget: { remaining: 1 },
4075
+ ...(this.peerOwnType() === 'human' ? { window_mode: 'tail' } : {}) });
4076
+ }
4077
+ }
3345
4078
  async onV2PushNotification(data, expectedVersion, inlineGeneration) {
3346
4079
  const client = this.runtime.client;
4080
+ if (client._p2pPeerPullEnabled) {
4081
+ await this.onPeerPush(data, expectedVersion);
4082
+ return;
4083
+ }
3347
4084
  const syncGeneration = this.captureInlineAckGeneration();
3348
4085
  if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
3349
4086
  return;
@@ -3561,6 +4298,14 @@ export class MessageDeliveryEngine {
3561
4298
  const client = this.runtime.client;
3562
4299
  if (!client._aid)
3563
4300
  return;
4301
+ this.ensurePeerContext();
4302
+ const rememberPeers = () => {
4303
+ const state = client._seqTracker.exportSyncState?.() ?? {};
4304
+ for (const [ns, triplet] of Object.entries(state)) {
4305
+ if (this.isReliableSeqNamespace(ns))
4306
+ this.committedSeqTrackerStates.set(ns, { ...triplet });
4307
+ }
4308
+ };
3564
4309
  try {
3565
4310
  const loadSync = client._tokenStore.loadAllSyncStates;
3566
4311
  if (typeof loadSync === 'function' && typeof client._seqTracker.restoreSyncState === 'function') {
@@ -3568,6 +4313,7 @@ export class MessageDeliveryEngine {
3568
4313
  if (state && Object.keys(state).length > 0) {
3569
4314
  state = this.migrateSeqStateGroupIds(state);
3570
4315
  client._seqTracker.restoreSyncState(state);
4316
+ rememberPeers();
3571
4317
  return;
3572
4318
  }
3573
4319
  }
@@ -3577,6 +4323,7 @@ export class MessageDeliveryEngine {
3577
4323
  if (state && Object.keys(state).length > 0) {
3578
4324
  state = this.migrateSeqStateGroupIds(state);
3579
4325
  client._seqTracker.restoreState(state);
4326
+ rememberPeers();
3580
4327
  return;
3581
4328
  }
3582
4329
  }
@@ -3590,6 +4337,7 @@ export class MessageDeliveryEngine {
3590
4337
  client._seqTracker.restoreSyncState(state);
3591
4338
  else
3592
4339
  client._seqTracker.restoreState(state);
4340
+ rememberPeers();
3593
4341
  }
3594
4342
  }
3595
4343
  }
@@ -3610,6 +4358,10 @@ export class MessageDeliveryEngine {
3610
4358
  }
3611
4359
  enqueueOnlineUnreadP2pHint(data) {
3612
4360
  const client = this.runtime.client;
4361
+ if (client._p2pPeerPullEnabled) {
4362
+ this.refreshPendingPeers();
4363
+ return;
4364
+ }
3613
4365
  if (!client._aid || !client._v2Session)
3614
4366
  return;
3615
4367
  const ns = `p2p:${client._aid}`;
@@ -3632,6 +4384,16 @@ export class MessageDeliveryEngine {
3632
4384
  const state = typeof client._seqTracker.exportSyncState === 'function'
3633
4385
  ? client._seqTracker.exportSyncState()
3634
4386
  : Object.fromEntries(Object.entries(client._seqTracker.exportState()).map(([ns, seq]) => [ns, { ack: Number(seq), tail: Number(seq), head: Number(seq) }]));
4387
+ this.ensurePeerContext();
4388
+ for (const ns of Object.keys(state)) {
4389
+ if (!this.isReliableSeqNamespace(ns))
4390
+ continue;
4391
+ const committed = this.committedSeqTrackerStates.get(ns);
4392
+ if (committed)
4393
+ state[ns] = { ...committed };
4394
+ else if (ns.startsWith('p2p.peer.v1/'))
4395
+ delete state[ns];
4396
+ }
3635
4397
  const pending = this.seqTrackerPending();
3636
4398
  if (pending.size > 0 && client._seqTrackerPendingPersistContext === context) {
3637
4399
  const liveNamespaces = new Set(Object.keys(state));
@@ -3659,9 +4421,12 @@ export class MessageDeliveryEngine {
3659
4421
  if (forceSeq !== undefined) {
3660
4422
  client._seqTracker.forceContiguousSeq(ns, seq);
3661
4423
  }
3662
- const state = typeof client._seqTracker.getSyncState === 'function'
3663
- ? client._seqTracker.getSyncState(ns)
3664
- : { ack: seq, tail: seq, head: seq };
4424
+ const state = this.committedSeqTrackerStates.get(ns) ?? (ns.startsWith('p2p.peer.v1/') ? undefined
4425
+ : typeof client._seqTracker.getSyncState === 'function'
4426
+ ? client._seqTracker.getSyncState(ns)
4427
+ : { ack: seq, tail: seq, head: seq });
4428
+ if (!state)
4429
+ return;
3665
4430
  this.mergeSeqTrackerPending(context, { [ns]: state });
3666
4431
  this.scheduleSeqTrackerFlush();
3667
4432
  }
@@ -3676,12 +4441,16 @@ export class MessageDeliveryEngine {
3676
4441
  if (![state.ack, state.tail, state.head].some((value) => Number(value) > 0))
3677
4442
  return;
3678
4443
  const [aid, deviceId, slotId] = JSON.parse(context);
4444
+ const tracker = client._seqTracker;
4445
+ const namespaceGeneration = this.seqTrackerNamespaceGenerations.get(ns) ?? 0;
3679
4446
  const pendingBeforeCommit = this.takeSeqTrackerPending(context, ns);
3680
4447
  try {
3681
4448
  await this.writeSeqTrackerState(aid, deviceId, slotId, { [ns]: state }, true);
3682
4449
  }
3683
4450
  catch (exc) {
3684
- this.restoreSeqTrackerPending(context, ns, pendingBeforeCommit);
4451
+ if (client._seqTracker === tracker && (this.seqTrackerNamespaceGenerations.get(ns) ?? 0) === namespaceGeneration) {
4452
+ this.restoreSeqTrackerPending(context, ns, pendingBeforeCommit);
4453
+ }
3685
4454
  throw exc;
3686
4455
  }
3687
4456
  client._clientLog.debug(`seq_tracker atomic commit: ns=${ns} A/T/H=${JSON.stringify(state)} context=${context}`);
@@ -3788,7 +4557,37 @@ export class MessageDeliveryEngine {
3788
4557
  }
3789
4558
  }
3790
4559
  async writeSeqTrackerState(aid, deviceId, slotId, state, throwOnError = false) {
3791
- await this.enqueueSeqTrackerWrite(() => this.writeSeqTrackerStateNow(aid, deviceId, slotId, state, throwOnError));
4560
+ const context = JSON.stringify([aid, deviceId, slotId]);
4561
+ const tracker = this.runtime.client._seqTracker;
4562
+ const namespaceGenerations = new Map(Object.keys(state).map((ns) => [ns, this.seqTrackerNamespaceGenerations.get(ns) ?? 0]));
4563
+ await this.enqueueSeqTrackerWrite(async () => {
4564
+ const current = context === this.currentSeqTrackerContext() && tracker === this.runtime.client._seqTracker;
4565
+ if (throwOnError && !current)
4566
+ throw new Error('seq tracker commit invalidated');
4567
+ if (current)
4568
+ this.ensurePeerContext();
4569
+ const writing = { ...state };
4570
+ if (!throwOnError && current) {
4571
+ for (const ns of Object.keys(writing)) {
4572
+ if (!this.isReliableSeqNamespace(ns))
4573
+ continue;
4574
+ const committed = this.committedSeqTrackerStates.get(ns);
4575
+ if (committed)
4576
+ writing[ns] = { ...committed };
4577
+ else if (ns.startsWith('p2p.peer.v1/'))
4578
+ delete writing[ns];
4579
+ }
4580
+ }
4581
+ await this.writeSeqTrackerStateNow(aid, deviceId, slotId, writing, throwOnError);
4582
+ if (throwOnError && context === this.currentSeqTrackerContext() && tracker === this.runtime.client._seqTracker) {
4583
+ for (const [ns, triplet] of Object.entries(writing)) {
4584
+ if (this.isReliableSeqNamespace(ns)
4585
+ && (this.seqTrackerNamespaceGenerations.get(ns) ?? 0) === namespaceGenerations.get(ns)) {
4586
+ this.committedSeqTrackerStates.set(ns, { ...triplet });
4587
+ }
4588
+ }
4589
+ }
4590
+ });
3792
4591
  }
3793
4592
  enqueueSeqTrackerWrite(operation) {
3794
4593
  const run = this.seqTrackerWriteChain.then(operation, operation);
@@ -3841,10 +4640,16 @@ export class MessageDeliveryEngine {
3841
4640
  const client = this.runtime.client;
3842
4641
  if (!client._aid || !ns)
3843
4642
  return;
4643
+ this.ensurePeerContext();
3844
4644
  const aid = client._aid;
3845
4645
  const deviceId = client._deviceId;
3846
4646
  const slotId = client._slotId;
3847
4647
  const seq = client._seqTracker.getContiguousSeq(ns);
4648
+ if (seq <= 0) {
4649
+ this.seqTrackerNamespaceGenerations.set(ns, (this.seqTrackerNamespaceGenerations.get(ns) ?? 0) + 1);
4650
+ this.committedSeqTrackerStates.delete(ns);
4651
+ this.initializedPeers.delete(ns);
4652
+ }
3848
4653
  this.dropSeqTrackerPending(ns);
3849
4654
  const saveSeq = client._tokenStore.saveSeq;
3850
4655
  if (seq > 0 && typeof saveSeq === 'function') {
@@ -3902,7 +4707,9 @@ export class MessageDeliveryEngine {
3902
4707
  if (next < 0)
3903
4708
  next = 0;
3904
4709
  if (ns) {
3905
- const committedAck = client._seqTracker.getContiguousSeq(ns);
4710
+ this.ensurePeerContext();
4711
+ const localAck = client._seqTracker.getContiguousSeq(ns);
4712
+ const committedAck = Math.min(localAck, this.committedSeqTrackerStates.get(ns)?.ack ?? localAck);
3906
4713
  if (next > committedAck) {
3907
4714
  client._clientLog.warn(`ack clamp: method=${method} ${field}=${original} > committed_ack=${committedAck}, clamp`);
3908
4715
  next = committedAck;
@@ -3959,6 +4766,8 @@ export class MessageDeliveryEngine {
3959
4766
  const queue = client._pendingOrderedMsgs.get(ns);
3960
4767
  if (!queue || queue.size === 0)
3961
4768
  return;
4769
+ const page = this.currentApplicationPage();
4770
+ let deliveredThrough = client._seqTracker.getContiguousSeq(ns);
3962
4771
  let delivered = false;
3963
4772
  const rootTrigger = this.deliveryChangedTrigger(source);
3964
4773
  const drainContexts = new Map();
@@ -3966,16 +4775,17 @@ export class MessageDeliveryEngine {
3966
4775
  while (true) {
3967
4776
  if (pullScoped)
3968
4777
  this.ensurePullOperationCurrent(ns);
3969
- const contig = client._seqTracker.getContiguousSeq(ns);
4778
+ const contig = page ? Math.max(deliveredThrough, client._seqTracker.getContiguousSeq(ns)) : client._seqTracker.getContiguousSeq(ns);
3970
4779
  const ready = [...queue.keys()]
3971
- .filter((seq) => seq <= Math.max(contig, throughSeq ?? contig) && (beforeSeq === undefined || seq < beforeSeq))
4780
+ .filter((seq) => !page?.consumed.has(JSON.stringify(['ordered', ns, seq]))
4781
+ && seq <= Math.max(contig, throughSeq ?? contig) && (beforeSeq === undefined || seq < beforeSeq))
3972
4782
  .sort((a, b) => a - b);
3973
4783
  let seq = ready[0];
3974
4784
  if (seq === undefined) {
3975
4785
  const nextSeq = Math.max(contig, throughSeq ?? contig) + 1;
3976
4786
  if (beforeSeq !== undefined && nextSeq >= beforeSeq)
3977
4787
  break;
3978
- if (!queue.has(nextSeq))
4788
+ if (!queue.has(nextSeq) || page?.consumed.has(JSON.stringify(['ordered', ns, nextSeq])))
3979
4789
  break;
3980
4790
  seq = nextSeq;
3981
4791
  }
@@ -3984,10 +4794,11 @@ export class MessageDeliveryEngine {
3984
4794
  const item = queue.get(seq);
3985
4795
  if (!item)
3986
4796
  continue;
3987
- if (client._pushedSeqs.get(ns)?.has(seq)) {
3988
- queue.delete(seq);
4797
+ if (this.hasPublishedSeq(ns, seq)) {
4798
+ this.consumeOrderedMessage(ns, seq, queue);
3989
4799
  client._clientLog.debug(`publish ordered drain skipped duplicate: ns=${ns}, seq=${seq}, event=${item.event}`);
3990
- client._markOrderedSeqDelivered(ns, seq);
4800
+ this.completeApplicationDelivery(() => client._markOrderedSeqDelivered(ns, seq));
4801
+ deliveredThrough = Math.max(deliveredThrough, seq);
3991
4802
  continue;
3992
4803
  }
3993
4804
  const itemSource = item.source ?? source;
@@ -4007,12 +4818,13 @@ export class MessageDeliveryEngine {
4007
4818
  throw new Error('pull invalidated');
4008
4819
  if (pullScoped)
4009
4820
  this.ensurePullOperationCurrent(ns);
4010
- queue.delete(seq);
4821
+ this.consumeOrderedMessage(ns, seq, queue);
4011
4822
  if (item.event === 'group.changed' && isJsonObject(item.payload)
4012
4823
  && item.payload.action === 'dissolved')
4013
4824
  break;
4014
4825
  this.markPublishedSeq(ns, seq);
4015
- client._markOrderedSeqDelivered(ns, seq);
4826
+ this.completeApplicationDelivery(() => client._markOrderedSeqDelivered(ns, seq));
4827
+ deliveredThrough = Math.max(deliveredThrough, seq);
4016
4828
  if (throughSeq !== undefined)
4017
4829
  throughSeq = Math.max(throughSeq, seq);
4018
4830
  delivered = true;
@@ -4122,6 +4934,87 @@ export class MessageDeliveryEngine {
4122
4934
  return true;
4123
4935
  }
4124
4936
  async publishPulledMessage(event, ns, seq, payload, persist = true, source = 'pull') {
4937
+ const client = this.runtime.client;
4938
+ const seqNum = Number(seq);
4939
+ if (!client._p2pPeerPullEnabled || event !== 'message.received' || source === 'history'
4940
+ || !Number.isSafeInteger(seqNum) || seqNum <= 0 || !payload || typeof payload !== 'object'
4941
+ || payload.storage === 'volatile') {
4942
+ return this.publishPulledMessageInternal(event, ns, seq, payload, persist, source);
4943
+ }
4944
+ const context = this.ensurePeerContext();
4945
+ const generation = this.captureInlineAckGeneration();
4946
+ const ensureCurrent = () => {
4947
+ if (context !== this.ensurePeerContext() || generation !== this.captureInlineAckGeneration())
4948
+ throw new Error('delivery invalidated');
4949
+ if (this.isPullScopedSource(source))
4950
+ this.ensurePullOperationCurrent(ns);
4951
+ };
4952
+ const value = payload;
4953
+ const sender = String(value.sender_aid ?? value.from_aid ?? value.from ?? this.peerSender(ns));
4954
+ if (!sender)
4955
+ return this.publishPulledMessageInternal(event, ns, seq, payload, persist, source);
4956
+ const messageId = String(value.message_id ?? '');
4957
+ const key = JSON.stringify([context, event, seqNum]);
4958
+ const page = this.currentApplicationPage();
4959
+ const bindingMatches = (receipt) => receipt.sender_aid === sender
4960
+ && (!receipt.message_id || !messageId || receipt.message_id === messageId);
4961
+ const local = page?.bindings.get(key);
4962
+ if (local) {
4963
+ if (!bindingMatches(local))
4964
+ throw new Error('P2P delivery receipt binding conflict');
4965
+ if (messageId && !local.message_id)
4966
+ local.message_id = messageId;
4967
+ return false;
4968
+ }
4969
+ const pending = this.deliveryInflight.get(key);
4970
+ if (pending) {
4971
+ if (client._dispatcher._isApplicationHandler?.())
4972
+ throw new Error('delivery cannot wait inside an application handler');
4973
+ await pending;
4974
+ ensureCurrent();
4975
+ const settled = this.settledDeliveries.get(key);
4976
+ if (settled && !bindingMatches(settled))
4977
+ throw new Error('P2P delivery receipt binding conflict');
4978
+ if (!messageId || settled?.message_id)
4979
+ return false;
4980
+ }
4981
+ const operation = Promise.resolve().then(async () => {
4982
+ ensureCurrent();
4983
+ let settled = this.settledDeliveries.get(key);
4984
+ if (settled && !bindingMatches(settled))
4985
+ throw new Error('P2P delivery receipt binding conflict');
4986
+ if (settled && messageId && !settled.message_id) {
4987
+ settled = { ...settled, message_id: messageId };
4988
+ }
4989
+ let published = false;
4990
+ if (!settled) {
4991
+ if (!this.isSelfSentP2PMessage(payload)) {
4992
+ await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, source));
4993
+ published = true;
4994
+ }
4995
+ ensureCurrent();
4996
+ settled = { sender_aid: sender, ...(messageId ? { message_id: messageId } : {}) };
4997
+ }
4998
+ ensureCurrent();
4999
+ const binding = settled;
5000
+ page?.bindings.set(key, binding);
5001
+ this.completeApplicationDelivery(() => {
5002
+ this.settledDeliveries.set(key, binding);
5003
+ while (this.settledDeliveries.size > PUSHED_SEQS_LIMIT) {
5004
+ const first = this.settledDeliveries.keys().next().value;
5005
+ this.settledDeliveries.delete(first);
5006
+ }
5007
+ });
5008
+ this.markPublishedSeq(ns, seqNum);
5009
+ return published;
5010
+ });
5011
+ const completion = page ? operation.then(async (result) => { await page.completion; return result; }) : operation;
5012
+ this.deliveryInflight.set(key, completion);
5013
+ void completion.finally(() => { if (this.deliveryInflight.get(key) === completion)
5014
+ this.deliveryInflight.delete(key); }).catch(() => { });
5015
+ return await operation;
5016
+ }
5017
+ async publishPulledMessageInternal(event, ns, seq, payload, persist = true, source = 'pull') {
4125
5018
  const client = this.runtime.client;
4126
5019
  const generation = this.captureInlineAckGeneration();
4127
5020
  const pullScoped = this.isPullScopedSource(source);
@@ -4144,9 +5037,9 @@ export class MessageDeliveryEngine {
4144
5037
  return true;
4145
5038
  }
4146
5039
  const queue = client._pendingOrderedMsgs.get(ns);
4147
- if (client._pushedSeqs.get(ns)?.has(seqNum)) {
5040
+ if (this.hasPublishedSeq(ns, seqNum)) {
4148
5041
  client._clientLog.debug(`publish pulled skipped duplicate: event=${event}, ns=${ns}, seq=${seqNum}`);
4149
- queue?.delete(seqNum);
5042
+ this.consumeOrderedMessage(ns, seqNum, queue);
4150
5043
  if (queue && queue.size === 0)
4151
5044
  client._pendingOrderedMsgs.delete(ns);
4152
5045
  return false;
@@ -4165,7 +5058,7 @@ export class MessageDeliveryEngine {
4165
5058
  throw new Error('pull invalidated');
4166
5059
  if (pullScoped)
4167
5060
  this.ensurePullOperationCurrent(ns);
4168
- queue?.delete(seqNum);
5061
+ this.consumeOrderedMessage(ns, seqNum, queue);
4169
5062
  if (queue && queue.size === 0)
4170
5063
  client._pendingOrderedMsgs.delete(ns);
4171
5064
  this.markPublishedSeq(ns, seqNum);
@@ -4182,7 +5075,7 @@ export class MessageDeliveryEngine {
4182
5075
  throw new Error('pull invalidated');
4183
5076
  if (pullScoped)
4184
5077
  this.ensurePullOperationCurrent(ns);
4185
- queue?.delete(seqNum);
5078
+ this.consumeOrderedMessage(ns, seqNum, queue);
4186
5079
  if (queue && queue.size === 0)
4187
5080
  client._pendingOrderedMsgs.delete(ns);
4188
5081
  this.markPublishedSeq(ns, seqNum);