@agentunion/fastaun 0.5.19 → 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 (129) hide show
  1. package/CHANGELOG.md +176 -78
  2. package/_packed_docs/CHANGELOG.md +176 -78
  3. package/_packed_docs/INDEX.md +288 -269
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +101 -95
  5. package/_packed_docs/agent.md/SCHEMA.md +7 -7
  6. 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
  7. 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
  8. 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
  9. 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
  10. package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +11 -11
  11. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +372 -331
  12. package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
  13. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +676 -676
  14. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +8 -12
  15. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +445 -0
  16. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.0.md +161 -0
  17. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +666 -0
  18. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
  19. package/_packed_docs/protocol/agent.md/SCHEMA.md +1 -1
  20. 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
  21. 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
  22. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
  23. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
  24. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
  25. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +70 -63
  26. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +43 -40
  27. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +179 -19
  28. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +50 -49
  29. package/_packed_docs/sdk/09-group-rpc-manual.md +261 -254
  30. package/_packed_docs/sdk/09-message-rpc-manual.md +253 -160
  31. package/_packed_docs/sdk/09-storage-rpc-manual.md +18 -26
  32. 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
  33. 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
  34. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +105 -92
  35. package/_packed_docs/sdk/INDEX.md +197 -167
  36. package/_packed_docs/sdk/Ownership-SDK/345/256/236/347/216/260/346/214/207/345/215/227.md +169 -0
  37. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/227.md +349 -0
  38. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +930 -0
  39. package/_packed_docs/sdk/Ownership-SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +270 -0
  40. 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 +363 -0
  41. 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 +553 -0
  42. package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -0
  43. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +480 -0
  44. package/_packed_docs/sdk/Ownership/347/256/241/347/220/206/344/270/223/351/242/230.md +559 -0
  45. package/_packed_docs/sdk/ProtectedHeaders/347/224/250/346/263/225/344/270/223/351/242/230.md +299 -0
  46. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +173 -0
  47. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +211 -0
  48. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/345/256/236/347/216/260/347/247/273/346/244/215/346/214/207/345/215/227.md +331 -0
  49. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +207 -0
  50. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/344/273/243/347/240/201/345/256/241/346/237/245/346/212/245/345/221/212.md +252 -0
  51. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/345/256/236/346/226/275/350/256/241/345/210/222.md +156 -0
  52. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/346/226/271/346/241/210.md +273 -0
  53. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/350/257/246/347/273/206/350/256/276/350/256/241.md +211 -0
  54. package/dist/agent-md-schema.d.ts +13 -1
  55. package/dist/agent-md-schema.js +63 -33
  56. package/dist/agent-md-schema.js.map +1 -1
  57. package/dist/agent-md.d.ts +30 -3
  58. package/dist/agent-md.js +293 -152
  59. package/dist/agent-md.js.map +1 -1
  60. package/dist/aid-store.d.ts +42 -0
  61. package/dist/aid-store.js +224 -44
  62. package/dist/aid-store.js.map +1 -1
  63. package/dist/aid.js +18 -13
  64. package/dist/aid.js.map +1 -1
  65. package/dist/auth.js +78 -2
  66. package/dist/auth.js.map +1 -1
  67. package/dist/cert-utils.d.ts +44 -1
  68. package/dist/cert-utils.js +157 -56
  69. package/dist/cert-utils.js.map +1 -1
  70. package/dist/client/delivery.d.ts +68 -1
  71. package/dist/client/delivery.js +1196 -135
  72. package/dist/client/delivery.js.map +1 -1
  73. package/dist/client/rpc-pipeline.d.ts +5 -1
  74. package/dist/client/rpc-pipeline.js +240 -111
  75. package/dist/client/rpc-pipeline.js.map +1 -1
  76. package/dist/client/v2-e2ee.d.ts +12 -2
  77. package/dist/client/v2-e2ee.js +842 -379
  78. package/dist/client/v2-e2ee.js.map +1 -1
  79. package/dist/client.d.ts +26 -2
  80. package/dist/client.js +420 -75
  81. package/dist/client.js.map +1 -1
  82. package/dist/config.d.ts +4 -0
  83. package/dist/config.js +1 -0
  84. package/dist/config.js.map +1 -1
  85. package/dist/events.d.ts +23 -4
  86. package/dist/events.js +171 -15
  87. package/dist/events.js.map +1 -1
  88. package/dist/facades.d.ts +43 -1
  89. package/dist/facades.js +368 -12
  90. package/dist/facades.js.map +1 -1
  91. package/dist/index.d.ts +7 -4
  92. package/dist/index.js +1 -1
  93. package/dist/index.js.map +1 -1
  94. package/dist/keystore/aid-db.d.ts +7 -2
  95. package/dist/keystore/aid-db.js +194 -43
  96. package/dist/keystore/aid-db.js.map +1 -1
  97. package/dist/keystore/index.d.ts +15 -1
  98. package/dist/keystore/local-token-store.d.ts +10 -2
  99. package/dist/keystore/local-token-store.js +81 -16
  100. package/dist/keystore/local-token-store.js.map +1 -1
  101. package/dist/org-protocol.d.ts +45 -0
  102. package/dist/org-protocol.js +829 -22
  103. package/dist/org-protocol.js.map +1 -1
  104. package/dist/ownership-v21.d.ts +40 -9
  105. package/dist/ownership-v21.js +174 -79
  106. package/dist/ownership-v21.js.map +1 -1
  107. package/dist/ownership-workflow.d.ts +22 -0
  108. package/dist/ownership-workflow.js +861 -66
  109. package/dist/ownership-workflow.js.map +1 -1
  110. package/dist/secret-store/file-store.js +84 -0
  111. package/dist/secret-store/file-store.js.map +1 -1
  112. package/dist/seq-tracker.d.ts +1 -0
  113. package/dist/seq-tracker.js +2 -2
  114. package/dist/seq-tracker.js.map +1 -1
  115. package/dist/tools/cross-sdk-agent.d.ts +67 -3
  116. package/dist/tools/cross-sdk-agent.js +575 -42
  117. package/dist/tools/cross-sdk-agent.js.map +1 -1
  118. package/dist/transport.d.ts +4 -0
  119. package/dist/transport.js +108 -44
  120. package/dist/transport.js.map +1 -1
  121. package/dist/types.d.ts +1 -0
  122. package/dist/v2/e2ee/encrypt-group.js +1 -0
  123. package/dist/v2/e2ee/encrypt-group.js.map +1 -1
  124. package/dist/v2/e2ee/encrypt-p2p.js +1 -0
  125. package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
  126. package/dist/v2/e2ee/types.d.ts +1 -0
  127. package/dist/version.d.ts +1 -1
  128. package/dist/version.js +1 -1
  129. 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';
@@ -9,16 +11,18 @@ const MESSAGE_RECALL_SEEN_LIMIT = 10_000;
9
11
  const GROUP_RECALL_SEEN_LIMIT = 10_000;
10
12
  const VOLATILE_SEEN_LIMIT = 10_000;
11
13
  const SEQ_TRACKER_PERSIST_FLUSH_DELAY_MS = 200;
14
+ const BACKGROUND_PULL_RETRY_DELAY_MS = 250;
12
15
  const DELIVERY_CHANGED_MESSAGE_EVENTS = new Set([
13
16
  'message.received',
14
17
  'group.message_created',
15
18
  ]);
19
+ const APPLICATION_DELIVERY_EVENTS = new Set(['message.received', 'message.recalled', 'group.message_created', 'group.message_recalled', 'group.changed']);
16
20
  const MAX_INLINE_PENDING_ACK_NAMESPACES = 4096;
17
21
  const APP_MESSAGE_ENVELOPE_KEYS = [
18
22
  'module_id', 'message_type', 'type', 'kind', 'version',
19
23
  'from', 'from_aid', 'sender_aid', 'to', 'to_aid', 'group_id',
20
24
  'timestamp', 'created_at', 'encrypted',
21
- 'context', 'protected_headers', 'headers', 'payload_type', 'mention_mode',
25
+ 'context', 'protected_headers', 'headers', 'payload_type', 'mention_mode', 'need_render',
22
26
  ];
23
27
  const RECALL_PAYLOAD_KEYS = [
24
28
  'message_ids', 'target_message_seqs',
@@ -80,6 +84,11 @@ export class MessageDeliveryEngine {
80
84
  pendingGroupPullUpper = null;
81
85
  onlineUnreadHintTargets = null;
82
86
  onlineUnreadHintTasks = null;
87
+ backgroundPullNeeds = new Map();
88
+ backgroundPullTasks = new Set();
89
+ backgroundPullCheckQueued = false;
90
+ backgroundPullTimer = null;
91
+ backgroundPullTimerAt = 0;
83
92
  pendingP2PInlineAcks = null;
84
93
  pendingGroupInlineAcks = null;
85
94
  pendingGroupInlineAckNamespaces = null;
@@ -94,9 +103,488 @@ export class MessageDeliveryEngine {
94
103
  volatileInFlightMessages = new Set();
95
104
  // 同一实例的 tracker 写入必须串行,避免旧的延迟 flush 覆盖新的 Forward 提交。
96
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();
97
127
  constructor(runtime) {
98
128
  this.runtime = runtime;
99
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
+ }
100
588
  isPullOperationCurrent(namespace = '') {
101
589
  const pipeline = this.runtime.client._rpcPipeline;
102
590
  return typeof pipeline?.isPullNamespaceCurrent === 'function'
@@ -164,15 +652,13 @@ export class MessageDeliveryEngine {
164
652
  const client = this.runtime.client;
165
653
  const response = result;
166
654
  const volatileMessages = method === 'message.pull' ? this.volatileMessages(response.messages) : [];
167
- if (volatileMessages.length > 0) {
168
- await this.publishPlainVolatileMessages(volatileMessages, 'pull', true);
169
- }
170
655
  this.ensurePullOperationCurrent(ns);
171
656
  this.syncStarted(ns);
172
657
  const messages = deduplicatePlainForwardPageMessages(response.messages, afterSeq);
173
658
  const coordinator = this.forwardCoordinator();
174
659
  const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
175
660
  const pageInlineGeneration = this.captureInlineAckGeneration();
661
+ const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
176
662
  const deferredServerCursor = Number(coordinator.pendingForwardCursor(ns, pageContigBefore) ?? 0);
177
663
  const deferredForwardAck = Number(coordinator.pendingForwardAck(ns, pageContigBefore) ?? 0);
178
664
  let pageTrackerSnapshot = null;
@@ -196,44 +682,51 @@ export class MessageDeliveryEngine {
196
682
  let publishedCount = 0;
197
683
  let committed = false;
198
684
  try {
199
- for (const rawMessage of messages) {
200
- this.ensurePullOperationCurrent(ns);
201
- const seq = positiveSafeSequenceHint(rawMessage.seq);
202
- if (method === 'message.pull') {
203
- const appEvent = this.p2pAppEventForMessage(rawMessage);
204
- if (await this.publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false)) {
205
- publishedCount += 1;
206
- }
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) {
207
689
  this.ensurePullOperationCurrent(ns);
208
- continue;
209
- }
210
- const message = normalizeGroupMentionMode(rawMessage);
211
- if (this.recallEventFromGroupMessage(message)) {
212
- 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
+ }
213
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)) {
214
708
  this.markPublishedSeq(ns, seq);
709
+ }
710
+ else if (await this.publishPulledMessage('group.message_created', ns, seq, message, false)) {
711
+ this.ensurePullOperationCurrent(ns);
215
712
  publishedCount += 1;
216
713
  }
217
714
  }
218
- else if (this.isSelfSentGroupMessage(message)) {
219
- this.markPublishedSeq(ns, seq);
220
- }
221
- else if (await this.publishPulledMessage('group.message_created', ns, seq, message, false)) {
222
- this.ensurePullOperationCurrent(ns);
223
- publishedCount += 1;
224
- }
225
- }
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
+ });
226
724
  this.ensurePullOperationCurrent(ns);
227
725
  if (pageInlineGeneration !== this.captureInlineAckGeneration())
228
726
  throw new Error('pull invalidated');
229
- const previousWindow = client._seqTracker.getSyncState(ns);
230
- const scanTarget = Math.max(pageContigBefore, ...messages.map((message) => positiveSafeSequenceHint(message.seq)), positiveSafeSequenceHint(response.contiguous_seq), retentionFloor, visibilityFloor, deferredServerCursor);
231
- const deliveryTarget = scanTarget >= previousWindow.tail - 1
232
- ? Math.max(scanTarget, previousWindow.head) : scanTarget;
233
- await this.drainOrderedMessages(ns, undefined, false, false, 'pull', undefined, deliveryTarget);
234
- this.ensurePullOperationCurrent(ns);
235
- if (pageInlineGeneration !== this.captureInlineAckGeneration())
727
+ if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
236
728
  throw new Error('pull invalidated');
729
+ this.rememberCommittedSeqState(ns);
237
730
  pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
238
731
  trackerMutated = true;
239
732
  client._seqTracker.onPullResult(ns, messages, afterSeq, Number.isSafeInteger(result.contiguous_seq)
@@ -249,11 +742,14 @@ export class MessageDeliveryEngine {
249
742
  this.ensurePullOperationCurrent(ns);
250
743
  if (pageInlineGeneration !== this.captureInlineAckGeneration())
251
744
  throw new Error('pull invalidated');
745
+ if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
746
+ throw new Error('pull invalidated');
252
747
  }
253
748
  committed = true;
254
749
  }
255
750
  catch (exc) {
256
751
  if (trackerMutated && pageInlineGeneration === this.captureInlineAckGeneration() && this.isPullOperationCurrent(ns)
752
+ && this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration)
257
753
  && !committed && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
258
754
  (client._seqTracker._restoreNamespaceSnapshotPreservingPush ?? client._seqTracker.restoreNamespaceSnapshot)
259
755
  .call(client._seqTracker, ns, pageTrackerSnapshot);
@@ -324,23 +820,39 @@ export class MessageDeliveryEngine {
324
820
  this.volatileCursor = '';
325
821
  this.volatileSeenMessages.clear();
326
822
  this.volatileInFlightMessages.clear();
823
+ for (const completion of this.volatileCompletions.values())
824
+ completion.reject(new Error('delivery invalidated'));
825
+ this.volatileCompletions.clear();
327
826
  }
328
827
  volatilePullCursor() {
329
828
  this.ensureVolatileContext();
330
829
  return this.volatileCursor;
331
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
+ }
332
842
  resetVolatilePullCursor() {
333
843
  this.ensureVolatileContext();
334
844
  this.volatileCursor = '';
335
845
  }
336
846
  volatileSeenKey(message) {
337
847
  const messageId = String(message.message_id ?? '').trim();
338
- const sender = String(message.from_aid ?? message.from ?? '').trim();
848
+ const sender = String(message.sender_aid ?? message.from_aid ?? message.from ?? '').trim();
339
849
  const cursor = String(message.volatile_cursor ?? '').trim();
340
- return `${sender}|${messageId || cursor}`;
850
+ return JSON.stringify([sender, messageId, cursor]);
341
851
  }
342
852
  isVolatileSeen(message) {
343
853
  this.ensureVolatileContext();
854
+ if (this.currentApplicationPage()?.volatileSeen.has(this.volatileSeenKey(message)))
855
+ return true;
344
856
  const now = Date.now();
345
857
  for (const [key, expiresAt] of this.volatileSeenMessages) {
346
858
  if (expiresAt <= now)
@@ -350,6 +862,12 @@ export class MessageDeliveryEngine {
350
862
  }
351
863
  markVolatileSeen(message) {
352
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
+ }
353
871
  const now = Date.now();
354
872
  const expiresAt = Math.max(now + 60_000, Number(message.expires_at ?? now + 1_800_000) || now + 1_800_000);
355
873
  this.volatileSeenMessages.set(this.volatileSeenKey(message), expiresAt);
@@ -367,11 +885,38 @@ export class MessageDeliveryEngine {
367
885
  if (this.volatileInFlightMessages.has(key))
368
886
  return 'inflight';
369
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
+ });
370
901
  return 'claimed';
371
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
+ }
372
911
  finishVolatileDelivery(message) {
373
912
  this.ensureVolatileContext();
374
- 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);
375
920
  }
376
921
  publicVolatileMessage(message) {
377
922
  const result = this.stripInternalSenderDeviceFields(message);
@@ -379,15 +924,27 @@ export class MessageDeliveryEngine {
379
924
  return result;
380
925
  }
381
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
+ }
382
932
  const cursors = [...new Set(messages
383
933
  .map((message) => String(message.volatile_cursor ?? '').trim())
384
934
  .filter(Boolean))];
385
935
  if (cursors.length === 0)
386
936
  return undefined;
387
- 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 });
388
942
  if (advanceCursor) {
389
943
  this.ensureVolatileContext();
390
- 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] ?? '';
391
948
  }
392
949
  return result;
393
950
  }
@@ -400,8 +957,12 @@ export class MessageDeliveryEngine {
400
957
  processed.push(rawMessage);
401
958
  continue;
402
959
  }
403
- if (claim === 'inflight')
960
+ if (claim === 'inflight') {
961
+ await this.waitVolatileDelivery(message);
962
+ if (this.isVolatileSeen(message))
963
+ processed.push(rawMessage);
404
964
  continue;
965
+ }
405
966
  try {
406
967
  const appEvent = this.p2pAppEventForMessage(message);
407
968
  await this.runtime.client._publishAppEvent(appEvent.event, appEvent.payload, source);
@@ -422,6 +983,13 @@ export class MessageDeliveryEngine {
422
983
  this.deliveryChangedGeneration += 1;
423
984
  this.pendingDeliveryChanges = null;
424
985
  this.inlineAckGeneration += 1;
986
+ if (this.backgroundPullTimer)
987
+ clearTimeout(this.backgroundPullTimer);
988
+ this.backgroundPullTimer = null;
989
+ this.backgroundPullTimerAt = 0;
990
+ this.backgroundPullNeeds.clear();
991
+ this.backgroundPullTasks.clear();
992
+ this.backgroundPullCheckQueued = false;
425
993
  void this.runtime.client._rpcPipeline?.invalidatePulls?.();
426
994
  this.pendingP2PInlineAcks = null;
427
995
  this.pendingGroupInlineAcks = null;
@@ -432,6 +1000,12 @@ export class MessageDeliveryEngine {
432
1000
  this.onlineUnreadHintTasks = null;
433
1001
  this.realtimeTailHeads = null;
434
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();
435
1009
  }
436
1010
  deliveryChangedNamespace(event, payload) {
437
1011
  if (event === 'group.message_created' && isJsonObject(payload)) {
@@ -693,6 +1267,7 @@ export class MessageDeliveryEngine {
693
1267
  }
694
1268
  endRealtimeSync(ns) {
695
1269
  this.realtimeSyncing?.delete(ns);
1270
+ this.requestBackgroundPullRecovery();
696
1271
  this.runtime.client._rpcPipeline?.requestPullDrainedCheck?.();
697
1272
  }
698
1273
  hasPendingPull(ns) {
@@ -702,7 +1277,9 @@ export class MessageDeliveryEngine {
702
1277
  return this.pendingGroupPullUpper?.get(ns) ?? 0;
703
1278
  }
704
1279
  hasAnyPendingPull() {
705
- 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)
1282
+ || this.backgroundPullNeeds.size > 0
706
1283
  || Boolean(this.onlineUnreadHintTasks?.size)
707
1284
  || Boolean(this.realtimeSyncing?.size)
708
1285
  || Boolean(this.runtime.client._v2E2EE?.hasPendingSenderIKWork?.());
@@ -710,14 +1287,136 @@ export class MessageDeliveryEngine {
710
1287
  onPullGateIdle(ns) {
711
1288
  if (!ns)
712
1289
  return;
1290
+ this.requestBackgroundPullRecovery();
713
1291
  if (ns.startsWith('p2p:')) {
714
1292
  this.runtime.client._schedulePendingP2pPullIfNeeded?.(ns, 'pull-gate-idle');
715
1293
  return;
716
1294
  }
717
1295
  this.schedulePendingGroupPullIfNeeded(ns, 'pull-gate-idle');
718
1296
  }
1297
+ recordBackgroundPullNeed(ns, throughSeq = 0, probe = false) {
1298
+ const client = this.runtime.client;
1299
+ if (client._sessionOptions?.background_sync === false || !client._aid
1300
+ || !(ns === `p2p:${client._aid}` || ns.startsWith('group:') || ns.startsWith('group_event:')))
1301
+ return;
1302
+ const need = this.backgroundPullNeeds.get(ns) ?? {
1303
+ throughSeq: 0, hasMore: false, probe, lastAfter: -1, retryAt: 0,
1304
+ };
1305
+ need.throughSeq = Math.max(need.throughSeq, throughSeq);
1306
+ need.probe ||= probe;
1307
+ this.backgroundPullNeeds.set(ns, need);
1308
+ }
1309
+ observeBackgroundPullResponse(ns, afterSeq, result, limit = 50) {
1310
+ if (!isJsonObject(result))
1311
+ return;
1312
+ const response = result;
1313
+ const rows = ns.startsWith('group_event:') ? response.events : response.messages;
1314
+ if (!Array.isArray(rows))
1315
+ return;
1316
+ const cursor = isJsonObject(response.cursor)
1317
+ ? response.cursor : null;
1318
+ const throughSeq = Math.max(positiveSafeSequenceHint(cursor?.latest_seq), positiveSafeSequenceHint(response.latest_seq), positiveSafeSequenceHint(response.latest_message_seq), positiveSafeSequenceHint(response.latest_event_seq), ...rows.map((row) => row && typeof row === 'object' && row.storage !== 'volatile'
1319
+ ? positiveSafeSequenceHint(row.seq ?? row.event_seq) : 0));
1320
+ this.recordBackgroundPullNeed(ns, throughSeq);
1321
+ const need = this.backgroundPullNeeds.get(ns);
1322
+ if (!need || afterSeq < need.lastAfter)
1323
+ return;
1324
+ need.lastAfter = afterSeq;
1325
+ need.hasMore = response.has_more === true || positiveSafeSequenceHint(response.remaining) > 0
1326
+ || (!('has_more' in response) && limit > 0 && rows.length >= limit);
1327
+ need.probe = false;
1328
+ }
1329
+ finishBackgroundPull(ns, generation, afterSeq, failed) {
1330
+ if (generation !== this.captureInlineAckGeneration())
1331
+ return;
1332
+ const need = this.backgroundPullNeeds.get(ns);
1333
+ if (need) {
1334
+ if (failed)
1335
+ need.probe = true;
1336
+ else
1337
+ need.probe = false;
1338
+ need.retryAt = failed || this.syncState(ns).ack <= afterSeq
1339
+ ? Date.now() + BACKGROUND_PULL_RETRY_DELAY_MS : 0;
1340
+ }
1341
+ this.requestBackgroundPullRecovery();
1342
+ }
1343
+ requestBackgroundPullRecovery() {
1344
+ if (this.backgroundPullCheckQueued || (this.backgroundPullNeeds.size === 0 && !this.peerCatalogDirty && this.peerRecoveryNeeds.size === 0))
1345
+ return;
1346
+ this.backgroundPullCheckQueued = true;
1347
+ const generation = this.captureInlineAckGeneration();
1348
+ queueMicrotask(() => {
1349
+ this.backgroundPullCheckQueued = false;
1350
+ if (generation !== this.captureInlineAckGeneration())
1351
+ return;
1352
+ const client = this.runtime.client;
1353
+ if (client._sessionOptions?.background_sync === false || client.state !== ConnectionState.READY || client._closing)
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
+ }
1363
+ let retryAt = Infinity;
1364
+ for (const [ns, need] of this.backgroundPullNeeds) {
1365
+ if (this.backgroundPullTasks.has(ns) || this.realtimeSyncing?.has(ns) || this.onlineUnreadHintTasks?.has(ns))
1366
+ continue;
1367
+ const eventScope = ns.startsWith('group_event:');
1368
+ const filling = [...(client._gapFillDone?.keys?.() ?? [])].some((key) => eventScope
1369
+ ? String(key) === `group_event_pull:${ns}`
1370
+ : ns.startsWith('p2p:') ? String(key).startsWith('p2p:') : String(key).startsWith(`group_msg:${ns.slice(6)}:`));
1371
+ if (filling || (!eventScope && client._rpcPipeline?.hasPullActivity?.(ns, false))
1372
+ || (eventScope && ['pulling', 'processing', 'pulled'].includes(client._rpcPipeline?.getPullLifecycle?.(ns))))
1373
+ continue;
1374
+ if (this.syncState(ns).ack >= need.throughSeq && !need.hasMore && !need.probe) {
1375
+ this.backgroundPullNeeds.delete(ns);
1376
+ continue;
1377
+ }
1378
+ if (need.retryAt > Date.now()) {
1379
+ retryAt = Math.min(retryAt, need.retryAt);
1380
+ continue;
1381
+ }
1382
+ this.backgroundPullTasks.add(ns);
1383
+ const task = Promise.resolve().then(() => {
1384
+ if (generation !== this.captureInlineAckGeneration())
1385
+ return;
1386
+ return eventScope ? this.fillGroupEventGap(ns.slice('group_event:'.length))
1387
+ : ns.startsWith('group:') ? this.fillGroupGap(ns.slice(6)) : this.fillP2pGap();
1388
+ }).finally(() => {
1389
+ if (generation !== this.captureInlineAckGeneration())
1390
+ return;
1391
+ this.backgroundPullTasks.delete(ns);
1392
+ this.requestBackgroundPullRecovery();
1393
+ });
1394
+ client._safeAsync(task);
1395
+ }
1396
+ if (this.backgroundPullNeeds.size === 0) {
1397
+ if (this.backgroundPullTimer)
1398
+ clearTimeout(this.backgroundPullTimer);
1399
+ this.backgroundPullTimer = null;
1400
+ this.backgroundPullTimerAt = 0;
1401
+ client._rpcPipeline?.requestPullDrainedCheck?.();
1402
+ }
1403
+ if (Number.isFinite(retryAt) && (!this.backgroundPullTimer || retryAt < this.backgroundPullTimerAt)) {
1404
+ if (this.backgroundPullTimer)
1405
+ clearTimeout(this.backgroundPullTimer);
1406
+ this.backgroundPullTimerAt = retryAt;
1407
+ this.backgroundPullTimer = setTimeout(() => {
1408
+ this.backgroundPullTimer = null;
1409
+ this.backgroundPullTimerAt = 0;
1410
+ this.requestBackgroundPullRecovery();
1411
+ }, Math.max(0, retryAt - Date.now()));
1412
+ this.backgroundPullTimer.unref?.();
1413
+ }
1414
+ });
1415
+ }
719
1416
  onForegroundPullsDrained() {
720
1417
  const client = this.runtime.client;
1418
+ if (client._p2pPeerPullEnabled)
1419
+ return;
721
1420
  if (!client._aid || client._sessionOptions?.background_sync === false
722
1421
  || client.state !== ConnectionState.READY || client._closing)
723
1422
  return;
@@ -771,6 +1470,12 @@ export class MessageDeliveryEngine {
771
1470
  }
772
1471
  }
773
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
+ }
774
1479
  const client = this.runtime.client;
775
1480
  let pushed = client._pushedSeqs.get(ns);
776
1481
  if (!pushed) {
@@ -783,6 +1488,18 @@ export class MessageDeliveryEngine {
783
1488
  client._pushedSeqs.set(ns, new Set(keep));
784
1489
  }
785
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
+ }
786
1503
  enqueueOrderedMessage(ns, event, seq, payload, source = 'push') {
787
1504
  const client = this.runtime.client;
788
1505
  let queue = client._pendingOrderedMsgs.get(ns);
@@ -833,21 +1550,38 @@ export class MessageDeliveryEngine {
833
1550
  async publishOrderedGroupChanged(payload, source = 'ordered') {
834
1551
  const client = this.runtime.client;
835
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
+ }
836
1563
  const published = client._publishAppEvent('group.changed', payload, source);
837
1564
  if (isPromiseLike(published))
838
1565
  await published;
839
1566
  if (generation !== this.captureInlineAckGeneration())
840
1567
  throw new Error('pull invalidated');
841
- if (isJsonObject(payload)) {
842
- const eventPayload = payload;
843
- client._groupState?.handleGroupChangedV2Membership?.(eventPayload);
844
- if (eventPayload.action === 'dissolved') {
845
- const rawGroupId = eventPayload.group_id ?? eventPayload.group_aid ?? '';
846
- const groupId = String(rawGroupId).trim();
847
- if (groupId)
848
- 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
+ }
849
1583
  }
850
- }
1584
+ });
851
1585
  }
852
1586
  isInstanceScopedMessageEvent(event) {
853
1587
  return event === 'message.received'
@@ -951,6 +1685,7 @@ export class MessageDeliveryEngine {
951
1685
  setIfPresent('timestamp', firstValue(message.timestamp, message.created_at, message.t_server));
952
1686
  if ('encrypted' in message)
953
1687
  envelope.encrypted = Boolean(message.encrypted);
1688
+ envelope.need_render = message.need_render !== false;
954
1689
  const context = this.envelopeMetadata(message.context);
955
1690
  if (context)
956
1691
  envelope.context = context;
@@ -1018,6 +1753,7 @@ export class MessageDeliveryEngine {
1018
1753
  setIfPresent('version', firstValue(body.version, params.version));
1019
1754
  setIfPresent('timestamp', firstValue(params.timestamp, resultObj.timestamp, resultObj.created_at, resultObj.t_server, Date.now()));
1020
1755
  envelope.encrypted = Boolean(encrypted);
1756
+ envelope.need_render = params.need_render !== false;
1021
1757
  const context = this.envelopeMetadata(params.context);
1022
1758
  if (context)
1023
1759
  envelope.context = context;
@@ -1179,12 +1915,14 @@ export class MessageDeliveryEngine {
1179
1915
  throw new Error('pull invalidated');
1180
1916
  if (this.isPullScopedSource(source))
1181
1917
  this.ensurePullOperationCurrent(`p2p:${client._aid ?? ''}`);
1182
- seen.set(dedupKey, Date.now());
1183
- if (seen.size > MESSAGE_RECALL_SEEN_LIMIT) {
1184
- const drop = [...seen.entries()].sort((a, b) => a[1] - b[1]).slice(0, seen.size - MESSAGE_RECALL_SEEN_LIMIT);
1185
- for (const [oldKey] of drop)
1186
- seen.delete(oldKey);
1187
- }
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
+ });
1188
1926
  client._clientLog.debug(`message.recalled published: seq=${String(seq)} ids=${JSON.stringify(eventPayload.message_ids)}`);
1189
1927
  return true;
1190
1928
  }
@@ -1290,12 +2028,14 @@ export class MessageDeliveryEngine {
1290
2028
  throw new Error('pull invalidated');
1291
2029
  if (this.isPullScopedSource(source))
1292
2030
  this.ensurePullOperationCurrent(`group:${groupId}`);
1293
- seen.set(dedupKey, Date.now());
1294
- if (seen.size > GROUP_RECALL_SEEN_LIMIT) {
1295
- const drop = [...seen.entries()].sort((a, b) => a[1] - b[1]).slice(0, seen.size - GROUP_RECALL_SEEN_LIMIT);
1296
- for (const [oldKey] of drop)
1297
- seen.delete(oldKey);
1298
- }
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
+ });
1299
2039
  client._clientLog.debug(`group.message_recalled published: group=${groupId} seq=${String(seq)} ids=${JSON.stringify(eventPayload.message_ids)}`);
1300
2040
  return true;
1301
2041
  }
@@ -1340,9 +2080,18 @@ export class MessageDeliveryEngine {
1340
2080
  if (seqNum > client._seqTracker.getContiguousSeq(ns) && client._sessionOptions?.background_sync !== false) {
1341
2081
  this.fillGroupGap(groupId).catch((exc) => client._clientLog.warn(`background recall gap fill failed: ${formatDeliveryError(exc)}`));
1342
2082
  }
2083
+ await this.publishGroupRecallTombstone(groupId, seq, wrapped);
1343
2084
  }
1344
2085
  publishAppEvent(event, payload, source = 'direct', deliveryContext) {
1345
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
+ }
1346
2095
  if ((event === 'message.received' || event === 'group.message_created') && isJsonObject(payload)) {
1347
2096
  client._maybeAppendEchoTraceReceive(payload);
1348
2097
  }
@@ -1363,22 +2112,33 @@ export class MessageDeliveryEngine {
1363
2112
  }
1364
2113
  const generation = this.deliveryChangedGeneration;
1365
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
+ }
1366
2127
  const dispatcher = client._dispatcher;
1367
2128
  const result = ['message.received', 'group.message_created', 'message.recalled', 'group.message_recalled', 'group.changed'].includes(event)
1368
2129
  && typeof dispatcher._publishAndWait === 'function'
1369
- ? dispatcher._publishAndWait(event, normalized)
2130
+ ? dispatcher._publishAndWait(event, normalized, undefined, page?.reservation, page ? () => this.isApplicationPageCurrent(page) : undefined)
1370
2131
  : typeof dispatcher.enqueue === 'function'
1371
2132
  ? (dispatcher.enqueue(event, normalized), undefined)
1372
2133
  : typeof dispatcher.publishSyncAware === 'function'
1373
2134
  ? dispatcher.publishSyncAware(event, normalized)
1374
2135
  : dispatcher.publish?.(event, normalized);
1375
- const note = () => {
1376
- if (generation === this.deliveryChangedGeneration) {
1377
- this.noteDeliveryChanged(event, payload, source, deliveryContext);
1378
- }
1379
- };
1380
2136
  if (isPromiseLike(result))
1381
- 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
+ });
1382
2142
  note();
1383
2143
  return result;
1384
2144
  }
@@ -1575,6 +2335,10 @@ export class MessageDeliveryEngine {
1575
2335
  }
1576
2336
  async onRawMessageReceived(data) {
1577
2337
  const client = this.runtime.client;
2338
+ if (client._p2pPeerPullEnabled) {
2339
+ await this.onPeerPush(data);
2340
+ return;
2341
+ }
1578
2342
  const tStart = Date.now();
1579
2343
  if (isJsonObject(data)) {
1580
2344
  client._logMessageDebug('server-push', '_raw.message.received', 'message.received', data);
@@ -1617,6 +2381,11 @@ export class MessageDeliveryEngine {
1617
2381
  if (!this.messageTargetsCurrentInstance(msg))
1618
2382
  return;
1619
2383
  const seq = msg.seq;
2384
+ if (client._p2pPeerPullEnabled) {
2385
+ await this.onPeerPush(msg);
2386
+ await this.publishMessageRecallTombstone(seq, msg);
2387
+ return;
2388
+ }
1620
2389
  if (seq !== undefined && seq !== null && client._aid) {
1621
2390
  const ns = `p2p:${client._aid}`;
1622
2391
  const seqNum = Number(seq);
@@ -1625,7 +2394,6 @@ export class MessageDeliveryEngine {
1625
2394
  if (Number(seq) > client._seqTracker.getContiguousSeq(ns) && client._sessionOptions?.background_sync !== false) {
1626
2395
  this.fillP2pGap().catch((exc) => client._clientLog.warn(`background recall gap fill failed: ${formatDeliveryError(exc)}`));
1627
2396
  }
1628
- return;
1629
2397
  }
1630
2398
  await this.publishMessageRecallTombstone(seq, msg);
1631
2399
  }
@@ -1806,13 +2574,13 @@ export class MessageDeliveryEngine {
1806
2574
  const afterSeq = client._seqTracker.getContiguousSeq(ns);
1807
2575
  client._clientLog.debug(`auto pull group messages start: group=${groupId}, after_seq=${afterSeq}, seq=${String(notification.seq ?? '')}`);
1808
2576
  const operation = async () => {
1809
- const messages = await client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true });
2577
+ const messages = await client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true, background: true });
1810
2578
  this.prunePushedSeqs(ns);
1811
2579
  return messages.length;
1812
2580
  };
1813
2581
  if (client._rpcPipeline) {
1814
2582
  const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 });
1815
- await client._rpcPipeline.runPullSerialized(key, operation, true);
2583
+ await client._rpcPipeline.runPullNamespaceSerialized(key, operation, true);
1816
2584
  }
1817
2585
  else {
1818
2586
  await client._withBackgroundRpc(operation);
@@ -1820,18 +2588,25 @@ export class MessageDeliveryEngine {
1820
2588
  }
1821
2589
  async fillP2pGap() {
1822
2590
  const client = this.runtime.client;
2591
+ if (client._p2pPeerPullEnabled) {
2592
+ this.refreshPendingPeers();
2593
+ return;
2594
+ }
1823
2595
  if (client._sessionOptions?.background_sync === false)
1824
2596
  return;
1825
2597
  if (!client._aid)
1826
2598
  return;
1827
2599
  const ns = `p2p:${client._aid}`;
1828
2600
  const afterSeq = client._seqTracker.getContiguousSeq(ns);
2601
+ const generation = this.captureInlineAckGeneration();
1829
2602
  const dedupKey = `p2p:${afterSeq}`;
1830
2603
  if (client._gapFillDone.has(dedupKey))
1831
2604
  return;
1832
2605
  client._gapFillDone.set(dedupKey, Date.now());
2606
+ this.recordBackgroundPullNeed(ns, 0, true);
1833
2607
  client._clientLog.debug(`P2P message gap fill start: after_seq=${afterSeq}`);
1834
2608
  let filled = 0;
2609
+ let failed = false;
1835
2610
  try {
1836
2611
  // Gap Fill 本身就是 Forward Pull,必须沿用协调器的完整事务:
1837
2612
  // 原始页提交成功后由协调器负责 Piggyback/终端 ACK。
@@ -1844,11 +2619,15 @@ export class MessageDeliveryEngine {
1844
2619
  client._clientLog.debug(`P2P message gap fill done: after_seq=${afterSeq}, filled=${filled}`);
1845
2620
  }
1846
2621
  catch (exc) {
2622
+ failed = true;
1847
2623
  client._clientLog.warn(`P2P message gap fill failed: ${formatDeliveryError(exc)}`);
1848
2624
  }
1849
2625
  finally {
2626
+ if (generation !== this.captureInlineAckGeneration())
2627
+ return;
1850
2628
  client._gapFillDone.delete(dedupKey);
1851
2629
  client._schedulePendingP2pPullIfNeeded?.(ns, 'gap-fill-complete');
2630
+ this.finishBackgroundPull(ns, generation, afterSeq, failed);
1852
2631
  }
1853
2632
  }
1854
2633
  async fillGroupGap(groupId) {
@@ -1860,30 +2639,34 @@ export class MessageDeliveryEngine {
1860
2639
  return;
1861
2640
  const ns = `group:${groupId}`;
1862
2641
  const afterSeq = client._seqTracker.getContiguousSeq(ns);
2642
+ const generation = this.captureInlineAckGeneration();
1863
2643
  const dedupKey = `group_msg:${groupId}:${afterSeq}`;
1864
2644
  if (client._gapFillDone.has(dedupKey))
1865
2645
  return;
1866
2646
  client._gapFillDone.set(dedupKey, Date.now());
2647
+ this.recordBackgroundPullNeed(ns, 0, true);
1867
2648
  client._clientLog.debug(`group message gap fill start: group=${groupId}, after_seq=${afterSeq}`);
1868
2649
  let filled = 0;
2650
+ let failed = false;
1869
2651
  try {
1870
- const operation = () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true });
2652
+ const operation = () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true, background: true });
1871
2653
  const messages = client._rpcPipeline
1872
- ? 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)
1873
2655
  : await client._withBackgroundRpc(operation);
1874
2656
  filled = messages.length;
1875
2657
  this.prunePushedSeqs(ns);
1876
2658
  client._clientLog.debug(`group message gap fill done: group=${groupId}, after_seq=${afterSeq}, filled=${filled}`);
1877
2659
  }
1878
2660
  catch (exc) {
2661
+ failed = true;
1879
2662
  client._clientLog.warn(`group message gap fill failed: ${formatDeliveryError(exc)}`);
1880
2663
  }
1881
2664
  finally {
2665
+ if (generation !== this.captureInlineAckGeneration())
2666
+ return;
1882
2667
  client._gapFillDone.delete(dedupKey);
1883
- if (filled > 0 && client._seqTracker.getContiguousSeq(ns) > afterSeq) {
1884
- void this.fillGroupGap(groupId);
1885
- }
1886
2668
  this.schedulePendingGroupPullIfNeeded(ns, 'gap-fill-complete');
2669
+ this.finishBackgroundPull(ns, generation, afterSeq, failed);
1887
2670
  }
1888
2671
  }
1889
2672
  scheduleRealtimeWindowForward(ns, groupId = '') {
@@ -1944,6 +2727,7 @@ export class MessageDeliveryEngine {
1944
2727
  }
1945
2728
  }
1946
2729
  client._gapFillDone.set(dedupKey, Date.now());
2730
+ this.recordBackgroundPullNeed(ns, client._seqTracker.getMaxSeenSeq(ns), true);
1947
2731
  this.syncStarted(ns);
1948
2732
  let filled = 0;
1949
2733
  let continuationAfterSeq = 0;
@@ -1974,10 +2758,14 @@ export class MessageDeliveryEngine {
1974
2758
  }
1975
2759
  if (!isJsonObject(result))
1976
2760
  return;
2761
+ if (generation !== this.captureInlineAckGeneration())
2762
+ return;
2763
+ this.observeBackgroundPullResponse(ns, nextAfterSeq, result);
1977
2764
  const events = result.events;
1978
2765
  if (!Array.isArray(events))
1979
2766
  return;
1980
2767
  const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
2768
+ const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
1981
2769
  let pageTrackerSnapshot = null;
1982
2770
  let trackerMutated = false;
1983
2771
  const eventObjects = events.filter((evt) => isJsonObject(evt));
@@ -1993,35 +2781,37 @@ export class MessageDeliveryEngine {
1993
2781
  const eventSeqs = [];
1994
2782
  let hasDissolvedEvent = false;
1995
2783
  let publishedEventCount = 0;
1996
- for (const evt of eventObjects) {
1997
- const eventSeq = Number(evt.event_seq ?? 0);
1998
- if (Number.isFinite(eventSeq) && eventSeq > 0)
1999
- eventSeqs.push(eventSeq);
2000
- evt._from_gap_fill = true;
2001
- if (evt.action === 'dissolved')
2002
- hasDissolvedEvent = true;
2003
- const et = String(evt.event_type ?? '');
2004
- if (et !== 'group.message_created') {
2005
- const cs = evt.client_signature;
2006
- if (cs && isJsonObject(cs)) {
2007
- if (client._shouldSkipEventSignature(evt)) {
2008
- delete evt.client_signature;
2009
- }
2010
- else {
2011
- 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
+ }
2012
2808
  }
2809
+ if (et !== 'group.message_created')
2810
+ filled += 1;
2013
2811
  }
2014
- if (Number.isFinite(eventSeq) && eventSeq > 0 && !client._pushedSeqs.get(ns)?.has(eventSeq)) {
2015
- this.enqueueOrderedMessage(ns, 'group.changed', eventSeq, evt, 'pull');
2016
- publishedEventCount += 1;
2017
- }
2018
- }
2019
- if (et !== 'group.message_created')
2020
- filled += 1;
2021
- }
2022
- try {
2023
- const scanTarget = Math.max(pageContigBefore, effectiveFloor, positiveSafeSequenceHint(result.contiguous_seq), ...eventSeqs);
2024
- 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
+ });
2025
2815
  if (generation !== this.captureInlineAckGeneration())
2026
2816
  throw new Error('pull invalidated');
2027
2817
  this.ensurePullOperationCurrent(ns);
@@ -2029,6 +2819,9 @@ export class MessageDeliveryEngine {
2029
2819
  dissolved = true;
2030
2820
  break;
2031
2821
  }
2822
+ if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
2823
+ throw new Error('pull invalidated');
2824
+ this.rememberCommittedSeqState(ns);
2032
2825
  pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
2033
2826
  trackerMutated = true;
2034
2827
  client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq, Number.isSafeInteger(result.contiguous_seq)
@@ -2041,10 +2834,13 @@ export class MessageDeliveryEngine {
2041
2834
  this.ensurePullOperationCurrent(ns);
2042
2835
  if (generation !== this.captureInlineAckGeneration())
2043
2836
  throw new Error('pull invalidated');
2837
+ if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
2838
+ throw new Error('pull invalidated');
2044
2839
  }
2045
2840
  }
2046
2841
  catch (exc) {
2047
2842
  if (trackerMutated && generation === this.captureInlineAckGeneration() && this.isPullOperationCurrent(ns)
2843
+ && this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration)
2048
2844
  && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
2049
2845
  (client._seqTracker._restoreNamespaceSnapshotPreservingPush ?? client._seqTracker.restoreNamespaceSnapshot)
2050
2846
  .call(client._seqTracker, ns, pageTrackerSnapshot);
@@ -2085,6 +2881,7 @@ export class MessageDeliveryEngine {
2085
2881
  const nextAfter = Math.max(eventSeqs.length > 0 ? Math.max(...eventSeqs) : nextAfterSeq, nextAfterSeq);
2086
2882
  const pageAdvanced = eventObjects.length > 0 && nextAfter > nextAfterSeq;
2087
2883
  const needsMore = result.has_more === true
2884
+ || positiveSafeSequenceHint(result.remaining) > 0
2088
2885
  || client._seqTracker.getMaxSeenSeq(ns) > nextAfter;
2089
2886
  if (singlePage && pageAdvanced && needsMore) {
2090
2887
  continuationAfterSeq = nextAfter;
@@ -2103,19 +2900,22 @@ export class MessageDeliveryEngine {
2103
2900
  client._clientLog.warn(`group event gap fill failed: ${formatDeliveryError(exc)}`);
2104
2901
  }
2105
2902
  finally {
2106
- client._gapFillDone.delete(dedupKey);
2903
+ if (generation === this.captureInlineAckGeneration())
2904
+ client._gapFillDone.delete(dedupKey);
2107
2905
  if (legacyGateToken !== null && typeof client._releasePullGate === 'function') {
2108
2906
  client._releasePullGate(ns, legacyGateToken);
2109
2907
  }
2908
+ if (generation !== this.captureInlineAckGeneration())
2909
+ return;
2910
+ if (dissolved)
2911
+ this.backgroundPullNeeds.delete(ns);
2110
2912
  if (!dissolved && singlePage && continuationAfterSeq > afterSeq) {
2111
2913
  this.enqueueOnlineUnreadEventHint({
2112
2914
  group_id: groupId,
2113
2915
  _online_hint_after_event_seq: continuationAfterSeq,
2114
2916
  });
2115
2917
  }
2116
- else if (!dissolved && !singlePage && filled > 0 && client._seqTracker.getContiguousSeq(ns) > afterSeq) {
2117
- void this.fillGroupEventGap(groupId);
2118
- }
2918
+ this.finishBackgroundPull(ns, generation, afterSeq, failed);
2119
2919
  if (!client._rpcPipeline)
2120
2920
  this.syncStopped(failed ? 'aborted' : 'pull_drained');
2121
2921
  }
@@ -2125,6 +2925,7 @@ export class MessageDeliveryEngine {
2125
2925
  const groupId = String(data.group_id ?? '').trim();
2126
2926
  if (!groupId || client._sessionOptions?.background_sync === false)
2127
2927
  return;
2928
+ this.recordBackgroundPullNeed(`group_event:${groupId}`, positiveSafeSequenceHint(data.event_seq), true);
2128
2929
  const rawAfter = Number(data._online_hint_after_event_seq ?? 0);
2129
2930
  const afterSeq = Number.isSafeInteger(rawAfter) && rawAfter > 0 ? rawAfter : 0;
2130
2931
  client._safeAsync(this.fillGroupEventGap(groupId, {
@@ -2518,11 +3319,11 @@ export class MessageDeliveryEngine {
2518
3319
  window_mode: 'tail',
2519
3320
  after_seq: after,
2520
3321
  limit: pageLimit,
2521
- }, true);
3322
+ }, true, true);
2522
3323
  tailHead = this.syncState(ns).head;
2523
3324
  return tailResult;
2524
3325
  }
2525
- const opts = { gateLocked: true };
3326
+ const opts = { gateLocked: true, background };
2526
3327
  if (request.max_pages !== undefined)
2527
3328
  opts.maxPages = request.max_pages;
2528
3329
  const messages = await client._pullGroupV2(groupId, after, pageLimit, opts);
@@ -2531,7 +3332,7 @@ export class MessageDeliveryEngine {
2531
3332
  if (!client._rpcPipeline)
2532
3333
  return await invoke();
2533
3334
  const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, ...request });
2534
- return await client._rpcPipeline.runPullSerialized(key, invoke, background);
3335
+ return await client._rpcPipeline.runPullNamespaceSerialized(key, invoke, background);
2535
3336
  };
2536
3337
  if (order.length === 1 && order[0] === 'tail') {
2537
3338
  result = await run({ window_mode: 'tail', after_seq: this.syncState(ns).ack, limit: pageLimit }, false);
@@ -2582,13 +3383,13 @@ export class MessageDeliveryEngine {
2582
3383
  const maxPages = Math.max(1, Math.ceil(Math.max(0, throughSeq - ack) / pageLimit));
2583
3384
  const request = { group_id: groupId, after_seq: ack, limit: pageLimit, max_pages: maxPages };
2584
3385
  const invoke = async () => {
2585
- 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 });
2586
3387
  return { messages, raw_count: messages.length };
2587
3388
  };
2588
3389
  if (!client._rpcPipeline)
2589
3390
  return await invoke();
2590
3391
  const key = client._rpcPipeline.pullGateKeyForCall('group.pull', request);
2591
- return await client._rpcPipeline.runPullSerialized(key, invoke, background);
3392
+ return await client._rpcPipeline.runPullNamespaceSerialized(key, invoke, background);
2592
3393
  }
2593
3394
  syncState(ns) {
2594
3395
  const tracker = this.runtime.client._seqTracker;
@@ -2929,6 +3730,7 @@ export class MessageDeliveryEngine {
2929
3730
  return false;
2930
3731
  const client = this.runtime.client;
2931
3732
  const tracker = client._seqTracker;
3733
+ const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
2932
3734
  let snapshot = null;
2933
3735
  let trackerMutated = false;
2934
3736
  let delivered = false;
@@ -2945,6 +3747,9 @@ export class MessageDeliveryEngine {
2945
3747
  delivered = true;
2946
3748
  if (!this.inlineGenerationIsCurrent(generation))
2947
3749
  return false;
3750
+ if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
3751
+ return false;
3752
+ this.rememberCommittedSeqState(ns);
2948
3753
  snapshot = typeof tracker.snapshotNamespace === 'function' ? tracker.snapshotNamespace(ns) : null;
2949
3754
  trackerMutated = true;
2950
3755
  const needsPull = Boolean(tracker.onMessageSeq(ns, seq));
@@ -2963,13 +3768,14 @@ export class MessageDeliveryEngine {
2963
3768
  if (!this.inlineGenerationIsCurrent(generation))
2964
3769
  return false;
2965
3770
  await client._commitSeqTrackerState(ns);
2966
- if (!this.inlineGenerationIsCurrent(generation))
3771
+ if (!this.inlineGenerationIsCurrent(generation) || !this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
2967
3772
  return false;
2968
3773
  this.markPublishedSeq(ns, seq);
2969
3774
  return true;
2970
3775
  }
2971
3776
  catch (exc) {
2972
3777
  if (trackerMutated && this.inlineGenerationIsCurrent(generation)
3778
+ && this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration)
2973
3779
  && typeof tracker.restoreNamespaceSnapshot === 'function') {
2974
3780
  (tracker._restoreNamespaceSnapshotPreservingPush ?? tracker.restoreNamespaceSnapshot).call(tracker, ns, snapshot);
2975
3781
  }
@@ -2984,6 +3790,8 @@ export class MessageDeliveryEngine {
2984
3790
  const tracker = client._seqTracker;
2985
3791
  if (!this.inlineGenerationIsCurrent(generation) || !this.inlineStateIsCaughtUp(ns, seq))
2986
3792
  return false;
3793
+ const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
3794
+ this.rememberCommittedSeqState(ns);
2987
3795
  const snapshot = typeof tracker.snapshotNamespace === 'function' ? tracker.snapshotNamespace(ns) : null;
2988
3796
  try {
2989
3797
  const needsPull = Boolean(tracker.onMessageSeq(ns, seq));
@@ -2998,10 +3806,11 @@ export class MessageDeliveryEngine {
2998
3806
  throw new Error('inline realtime terminal cursor did not collapse after consume');
2999
3807
  }
3000
3808
  await client._commitSeqTrackerState(ns);
3001
- return this.inlineGenerationIsCurrent(generation);
3809
+ return this.inlineGenerationIsCurrent(generation) && this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration);
3002
3810
  }
3003
3811
  catch (exc) {
3004
3812
  if (this.inlineGenerationIsCurrent(generation)
3813
+ && this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration)
3005
3814
  && typeof tracker.restoreNamespaceSnapshot === 'function') {
3006
3815
  (tracker._restoreNamespaceSnapshotPreservingPush ?? tracker.restoreNamespaceSnapshot).call(tracker, ns, snapshot);
3007
3816
  }
@@ -3195,8 +4004,84 @@ export class MessageDeliveryEngine {
3195
4004
  }
3196
4005
  return 0;
3197
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
+ }
3198
4078
  async onV2PushNotification(data, expectedVersion, inlineGeneration) {
3199
4079
  const client = this.runtime.client;
4080
+ if (client._p2pPeerPullEnabled) {
4081
+ await this.onPeerPush(data, expectedVersion);
4082
+ return;
4083
+ }
4084
+ const syncGeneration = this.captureInlineAckGeneration();
3200
4085
  if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
3201
4086
  return;
3202
4087
  const pushData = isJsonObject(data) ? data : null;
@@ -3251,6 +4136,7 @@ export class MessageDeliveryEngine {
3251
4136
  this.retryPendingP2PInlineAck(ns, pushSeq, 'covered inline push');
3252
4137
  return;
3253
4138
  }
4139
+ this.recordBackgroundPullNeed(ns, pushSeq);
3254
4140
  const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
3255
4141
  if (pullActive || !this.beginRealtimeSync(ns)) {
3256
4142
  client._recordPendingP2pPull(ns, pushSeq);
@@ -3267,7 +4153,9 @@ export class MessageDeliveryEngine {
3267
4153
  client._clientLog.warn(`V2 P2P push auto-sync failed: ${formatDeliveryError(exc)}`);
3268
4154
  }
3269
4155
  finally {
3270
- if (inlineGeneration === undefined || this.inlineGenerationIsCurrent(inlineGeneration)) {
4156
+ if (syncGeneration === this.captureInlineAckGeneration()
4157
+ && (inlineGeneration === undefined || this.inlineGenerationIsCurrent(inlineGeneration))) {
4158
+ this.finishBackgroundPull(ns, syncGeneration, this.syncState(ns).ack, false);
3271
4159
  this.endRealtimeSync(ns);
3272
4160
  client._schedulePendingP2pPullIfNeeded?.(ns, 'push-auto-pull-complete');
3273
4161
  }
@@ -3279,6 +4167,9 @@ export class MessageDeliveryEngine {
3279
4167
  if (!groupId)
3280
4168
  return;
3281
4169
  const ns = `group:${groupId}`;
4170
+ const generation = this.captureInlineAckGeneration();
4171
+ const afterSeq = this.syncState(ns).ack;
4172
+ this.recordBackgroundPullNeed(ns, positiveSafeSequenceHint(data.seq));
3282
4173
  const targets = this.onlineUnreadHintTargets ?? new Map();
3283
4174
  const tasks = this.onlineUnreadHintTasks ?? new Map();
3284
4175
  this.onlineUnreadHintTargets = targets;
@@ -3286,10 +4177,13 @@ export class MessageDeliveryEngine {
3286
4177
  targets.set(ns, Math.max(targets.get(ns) ?? 0, positiveSafeSequenceHint(data.seq)));
3287
4178
  if (tasks.has(ns))
3288
4179
  return;
4180
+ let attemptAfterSeq = afterSeq;
3289
4181
  const task = Promise.resolve().then(async () => {
4182
+ let failed = false;
3290
4183
  try {
3291
4184
  while (true) {
3292
4185
  const ackBefore = this.syncState(ns).ack;
4186
+ attemptAfterSeq = ackBefore;
3293
4187
  await this.runGroupForwardRecovery(groupId, ns, 50, true, targets.get(ns) ?? 0);
3294
4188
  const ackAfter = this.syncState(ns).ack;
3295
4189
  if (ackAfter >= (targets.get(ns) ?? 0) || ackAfter <= ackBefore)
@@ -3297,12 +4191,14 @@ export class MessageDeliveryEngine {
3297
4191
  }
3298
4192
  }
3299
4193
  catch (exc) {
4194
+ failed = true;
3300
4195
  client._clientLog.debug(`online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
3301
4196
  }
3302
4197
  finally {
3303
4198
  targets.delete(ns);
3304
4199
  if (tasks.get(ns) === task)
3305
4200
  tasks.delete(ns);
4201
+ this.finishBackgroundPull(ns, generation, attemptAfterSeq, failed);
3306
4202
  client._rpcPipeline?.requestPullDrainedCheck?.();
3307
4203
  }
3308
4204
  });
@@ -3311,6 +4207,7 @@ export class MessageDeliveryEngine {
3311
4207
  }
3312
4208
  async onRawGroupV2MessageCreated(data, expectedVersion, inlineGeneration) {
3313
4209
  const client = this.runtime.client;
4210
+ const syncGeneration = this.captureInlineAckGeneration();
3314
4211
  if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
3315
4212
  return;
3316
4213
  if (!isJsonObject(data)) {
@@ -3373,6 +4270,7 @@ export class MessageDeliveryEngine {
3373
4270
  }
3374
4271
  return;
3375
4272
  }
4273
+ this.recordBackgroundPullNeed(ns, seq);
3376
4274
  const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
3377
4275
  if (pullActive || !this.beginRealtimeSync(ns)) {
3378
4276
  this.recordPendingGroupPull(ns, seq);
@@ -3388,7 +4286,9 @@ export class MessageDeliveryEngine {
3388
4286
  return;
3389
4287
  client._clientLog.warn(`V2 group push auto-sync failed: group=${groupId} err=${formatDeliveryError(exc)}`);
3390
4288
  }).finally(() => {
3391
- if (inlineGeneration === undefined || this.inlineGenerationIsCurrent(inlineGeneration)) {
4289
+ if (syncGeneration === this.captureInlineAckGeneration()
4290
+ && (inlineGeneration === undefined || this.inlineGenerationIsCurrent(inlineGeneration))) {
4291
+ this.finishBackgroundPull(ns, syncGeneration, this.syncState(ns).ack, false);
3392
4292
  this.endRealtimeSync(ns);
3393
4293
  this.schedulePendingGroupPullIfNeeded(ns, 'push-auto-pull-complete');
3394
4294
  }
@@ -3398,6 +4298,14 @@ export class MessageDeliveryEngine {
3398
4298
  const client = this.runtime.client;
3399
4299
  if (!client._aid)
3400
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
+ };
3401
4309
  try {
3402
4310
  const loadSync = client._tokenStore.loadAllSyncStates;
3403
4311
  if (typeof loadSync === 'function' && typeof client._seqTracker.restoreSyncState === 'function') {
@@ -3405,6 +4313,7 @@ export class MessageDeliveryEngine {
3405
4313
  if (state && Object.keys(state).length > 0) {
3406
4314
  state = this.migrateSeqStateGroupIds(state);
3407
4315
  client._seqTracker.restoreSyncState(state);
4316
+ rememberPeers();
3408
4317
  return;
3409
4318
  }
3410
4319
  }
@@ -3414,6 +4323,7 @@ export class MessageDeliveryEngine {
3414
4323
  if (state && Object.keys(state).length > 0) {
3415
4324
  state = this.migrateSeqStateGroupIds(state);
3416
4325
  client._seqTracker.restoreState(state);
4326
+ rememberPeers();
3417
4327
  return;
3418
4328
  }
3419
4329
  }
@@ -3427,6 +4337,7 @@ export class MessageDeliveryEngine {
3427
4337
  client._seqTracker.restoreSyncState(state);
3428
4338
  else
3429
4339
  client._seqTracker.restoreState(state);
4340
+ rememberPeers();
3430
4341
  }
3431
4342
  }
3432
4343
  }
@@ -3447,12 +4358,21 @@ export class MessageDeliveryEngine {
3447
4358
  }
3448
4359
  enqueueOnlineUnreadP2pHint(data) {
3449
4360
  const client = this.runtime.client;
4361
+ if (client._p2pPeerPullEnabled) {
4362
+ this.refreshPendingPeers();
4363
+ return;
4364
+ }
3450
4365
  if (!client._aid || !client._v2Session)
3451
4366
  return;
3452
4367
  const ns = `p2p:${client._aid}`;
4368
+ const generation = this.captureInlineAckGeneration();
4369
+ const afterSeq = this.syncState(ns).ack;
4370
+ this.recordBackgroundPullNeed(ns, positiveSafeSequenceHint(data.seq), true);
4371
+ let failed = false;
3453
4372
  client._safeAsync(this.runP2PForwardRecovery(ns, 50, true).catch((exc) => {
4373
+ failed = true;
3454
4374
  client._clientLog.debug(`P2P online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
3455
- }));
4375
+ }).finally(() => this.finishBackgroundPull(ns, generation, afterSeq, failed)));
3456
4376
  }
3457
4377
  async saveSeqTrackerState() {
3458
4378
  const client = this.runtime.client;
@@ -3464,6 +4384,16 @@ export class MessageDeliveryEngine {
3464
4384
  const state = typeof client._seqTracker.exportSyncState === 'function'
3465
4385
  ? client._seqTracker.exportSyncState()
3466
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
+ }
3467
4397
  const pending = this.seqTrackerPending();
3468
4398
  if (pending.size > 0 && client._seqTrackerPendingPersistContext === context) {
3469
4399
  const liveNamespaces = new Set(Object.keys(state));
@@ -3491,9 +4421,12 @@ export class MessageDeliveryEngine {
3491
4421
  if (forceSeq !== undefined) {
3492
4422
  client._seqTracker.forceContiguousSeq(ns, seq);
3493
4423
  }
3494
- const state = typeof client._seqTracker.getSyncState === 'function'
3495
- ? client._seqTracker.getSyncState(ns)
3496
- : { 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;
3497
4430
  this.mergeSeqTrackerPending(context, { [ns]: state });
3498
4431
  this.scheduleSeqTrackerFlush();
3499
4432
  }
@@ -3508,12 +4441,16 @@ export class MessageDeliveryEngine {
3508
4441
  if (![state.ack, state.tail, state.head].some((value) => Number(value) > 0))
3509
4442
  return;
3510
4443
  const [aid, deviceId, slotId] = JSON.parse(context);
4444
+ const tracker = client._seqTracker;
4445
+ const namespaceGeneration = this.seqTrackerNamespaceGenerations.get(ns) ?? 0;
3511
4446
  const pendingBeforeCommit = this.takeSeqTrackerPending(context, ns);
3512
4447
  try {
3513
4448
  await this.writeSeqTrackerState(aid, deviceId, slotId, { [ns]: state }, true);
3514
4449
  }
3515
4450
  catch (exc) {
3516
- this.restoreSeqTrackerPending(context, ns, pendingBeforeCommit);
4451
+ if (client._seqTracker === tracker && (this.seqTrackerNamespaceGenerations.get(ns) ?? 0) === namespaceGeneration) {
4452
+ this.restoreSeqTrackerPending(context, ns, pendingBeforeCommit);
4453
+ }
3517
4454
  throw exc;
3518
4455
  }
3519
4456
  client._clientLog.debug(`seq_tracker atomic commit: ns=${ns} A/T/H=${JSON.stringify(state)} context=${context}`);
@@ -3620,7 +4557,37 @@ export class MessageDeliveryEngine {
3620
4557
  }
3621
4558
  }
3622
4559
  async writeSeqTrackerState(aid, deviceId, slotId, state, throwOnError = false) {
3623
- 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
+ });
3624
4591
  }
3625
4592
  enqueueSeqTrackerWrite(operation) {
3626
4593
  const run = this.seqTrackerWriteChain.then(operation, operation);
@@ -3673,10 +4640,16 @@ export class MessageDeliveryEngine {
3673
4640
  const client = this.runtime.client;
3674
4641
  if (!client._aid || !ns)
3675
4642
  return;
4643
+ this.ensurePeerContext();
3676
4644
  const aid = client._aid;
3677
4645
  const deviceId = client._deviceId;
3678
4646
  const slotId = client._slotId;
3679
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
+ }
3680
4653
  this.dropSeqTrackerPending(ns);
3681
4654
  const saveSeq = client._tokenStore.saveSeq;
3682
4655
  if (seq > 0 && typeof saveSeq === 'function') {
@@ -3734,7 +4707,9 @@ export class MessageDeliveryEngine {
3734
4707
  if (next < 0)
3735
4708
  next = 0;
3736
4709
  if (ns) {
3737
- 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);
3738
4713
  if (next > committedAck) {
3739
4714
  client._clientLog.warn(`ack clamp: method=${method} ${field}=${original} > committed_ack=${committedAck}, clamp`);
3740
4715
  next = committedAck;
@@ -3791,6 +4766,8 @@ export class MessageDeliveryEngine {
3791
4766
  const queue = client._pendingOrderedMsgs.get(ns);
3792
4767
  if (!queue || queue.size === 0)
3793
4768
  return;
4769
+ const page = this.currentApplicationPage();
4770
+ let deliveredThrough = client._seqTracker.getContiguousSeq(ns);
3794
4771
  let delivered = false;
3795
4772
  const rootTrigger = this.deliveryChangedTrigger(source);
3796
4773
  const drainContexts = new Map();
@@ -3798,16 +4775,17 @@ export class MessageDeliveryEngine {
3798
4775
  while (true) {
3799
4776
  if (pullScoped)
3800
4777
  this.ensurePullOperationCurrent(ns);
3801
- const contig = client._seqTracker.getContiguousSeq(ns);
4778
+ const contig = page ? Math.max(deliveredThrough, client._seqTracker.getContiguousSeq(ns)) : client._seqTracker.getContiguousSeq(ns);
3802
4779
  const ready = [...queue.keys()]
3803
- .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))
3804
4782
  .sort((a, b) => a - b);
3805
4783
  let seq = ready[0];
3806
4784
  if (seq === undefined) {
3807
4785
  const nextSeq = Math.max(contig, throughSeq ?? contig) + 1;
3808
4786
  if (beforeSeq !== undefined && nextSeq >= beforeSeq)
3809
4787
  break;
3810
- if (!queue.has(nextSeq))
4788
+ if (!queue.has(nextSeq) || page?.consumed.has(JSON.stringify(['ordered', ns, nextSeq])))
3811
4789
  break;
3812
4790
  seq = nextSeq;
3813
4791
  }
@@ -3816,10 +4794,11 @@ export class MessageDeliveryEngine {
3816
4794
  const item = queue.get(seq);
3817
4795
  if (!item)
3818
4796
  continue;
3819
- if (client._pushedSeqs.get(ns)?.has(seq)) {
3820
- queue.delete(seq);
4797
+ if (this.hasPublishedSeq(ns, seq)) {
4798
+ this.consumeOrderedMessage(ns, seq, queue);
3821
4799
  client._clientLog.debug(`publish ordered drain skipped duplicate: ns=${ns}, seq=${seq}, event=${item.event}`);
3822
- client._markOrderedSeqDelivered(ns, seq);
4800
+ this.completeApplicationDelivery(() => client._markOrderedSeqDelivered(ns, seq));
4801
+ deliveredThrough = Math.max(deliveredThrough, seq);
3823
4802
  continue;
3824
4803
  }
3825
4804
  const itemSource = item.source ?? source;
@@ -3839,12 +4818,13 @@ export class MessageDeliveryEngine {
3839
4818
  throw new Error('pull invalidated');
3840
4819
  if (pullScoped)
3841
4820
  this.ensurePullOperationCurrent(ns);
3842
- queue.delete(seq);
4821
+ this.consumeOrderedMessage(ns, seq, queue);
3843
4822
  if (item.event === 'group.changed' && isJsonObject(item.payload)
3844
4823
  && item.payload.action === 'dissolved')
3845
4824
  break;
3846
4825
  this.markPublishedSeq(ns, seq);
3847
- client._markOrderedSeqDelivered(ns, seq);
4826
+ this.completeApplicationDelivery(() => client._markOrderedSeqDelivered(ns, seq));
4827
+ deliveredThrough = Math.max(deliveredThrough, seq);
3848
4828
  if (throughSeq !== undefined)
3849
4829
  throughSeq = Math.max(throughSeq, seq);
3850
4830
  delivered = true;
@@ -3954,6 +4934,87 @@ export class MessageDeliveryEngine {
3954
4934
  return true;
3955
4935
  }
3956
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') {
3957
5018
  const client = this.runtime.client;
3958
5019
  const generation = this.captureInlineAckGeneration();
3959
5020
  const pullScoped = this.isPullScopedSource(source);
@@ -3976,9 +5037,9 @@ export class MessageDeliveryEngine {
3976
5037
  return true;
3977
5038
  }
3978
5039
  const queue = client._pendingOrderedMsgs.get(ns);
3979
- if (client._pushedSeqs.get(ns)?.has(seqNum)) {
5040
+ if (this.hasPublishedSeq(ns, seqNum)) {
3980
5041
  client._clientLog.debug(`publish pulled skipped duplicate: event=${event}, ns=${ns}, seq=${seqNum}`);
3981
- queue?.delete(seqNum);
5042
+ this.consumeOrderedMessage(ns, seqNum, queue);
3982
5043
  if (queue && queue.size === 0)
3983
5044
  client._pendingOrderedMsgs.delete(ns);
3984
5045
  return false;
@@ -3997,7 +5058,7 @@ export class MessageDeliveryEngine {
3997
5058
  throw new Error('pull invalidated');
3998
5059
  if (pullScoped)
3999
5060
  this.ensurePullOperationCurrent(ns);
4000
- queue?.delete(seqNum);
5061
+ this.consumeOrderedMessage(ns, seqNum, queue);
4001
5062
  if (queue && queue.size === 0)
4002
5063
  client._pendingOrderedMsgs.delete(ns);
4003
5064
  this.markPublishedSeq(ns, seqNum);
@@ -4014,7 +5075,7 @@ export class MessageDeliveryEngine {
4014
5075
  throw new Error('pull invalidated');
4015
5076
  if (pullScoped)
4016
5077
  this.ensurePullOperationCurrent(ns);
4017
- queue?.delete(seqNum);
5078
+ this.consumeOrderedMessage(ns, seqNum, queue);
4018
5079
  if (queue && queue.size === 0)
4019
5080
  client._pendingOrderedMsgs.delete(ns);
4020
5081
  this.markPublishedSeq(ns, seqNum);