@agentunion/fastaun 0.5.8 → 0.5.10

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 (54) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/_packed_docs/CHANGELOG.md +64 -0
  3. package/_packed_docs/agent.md/examples//347/276/244/347/273/204-/345/274/200/345/217/221/345/233/242/351/230/237.md +21 -0
  4. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +6 -5
  5. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +43 -8
  6. package/_packed_docs/sdk/09-group-rpc-manual.md +18 -3
  7. package/_packed_docs/sdk/09-message-rpc-manual.md +34 -10
  8. package/dist/agent-md-schema.js +3 -0
  9. package/dist/agent-md-schema.js.map +1 -1
  10. package/dist/agent-md.js +5 -1
  11. package/dist/agent-md.js.map +1 -1
  12. package/dist/aid-store.js +1 -3
  13. package/dist/aid-store.js.map +1 -1
  14. package/dist/auth.d.ts +5 -2
  15. package/dist/auth.js +64 -27
  16. package/dist/auth.js.map +1 -1
  17. package/dist/client/delivery.d.ts +25 -5
  18. package/dist/client/delivery.js +355 -70
  19. package/dist/client/delivery.js.map +1 -1
  20. package/dist/client/group-state.js +8 -8
  21. package/dist/client/group-state.js.map +1 -1
  22. package/dist/client/lifecycle.js +25 -22
  23. package/dist/client/lifecycle.js.map +1 -1
  24. package/dist/client/rpc-pipeline.d.ts +12 -0
  25. package/dist/client/rpc-pipeline.js +182 -32
  26. package/dist/client/rpc-pipeline.js.map +1 -1
  27. package/dist/client/v2-e2ee.d.ts +1 -0
  28. package/dist/client/v2-e2ee.js +55 -21
  29. package/dist/client/v2-e2ee.js.map +1 -1
  30. package/dist/client.js +108 -38
  31. package/dist/client.js.map +1 -1
  32. package/dist/discovery.d.ts +1 -0
  33. package/dist/discovery.js +3 -0
  34. package/dist/discovery.js.map +1 -1
  35. package/dist/events.d.ts +23 -8
  36. package/dist/events.js +120 -26
  37. package/dist/events.js.map +1 -1
  38. package/dist/facades.js +7 -3
  39. package/dist/facades.js.map +1 -1
  40. package/dist/index.d.ts +1 -1
  41. package/dist/index.js.map +1 -1
  42. package/dist/net.d.ts +3 -0
  43. package/dist/net.js +13 -0
  44. package/dist/net.js.map +1 -1
  45. package/dist/register-flow.js +15 -111
  46. package/dist/register-flow.js.map +1 -1
  47. package/dist/transport.d.ts +32 -3
  48. package/dist/transport.js +758 -33
  49. package/dist/transport.js.map +1 -1
  50. package/dist/version.d.ts +1 -1
  51. package/dist/version.js +1 -1
  52. package/dist/version.js.map +1 -1
  53. package/package.json +1 -1
  54. package/_packed_docs//345/217/221/345/270/203/346/212/245/345/221/212-0.5.6.md +0 -260
@@ -1,11 +1,16 @@
1
1
  import { slotIsolationKey } from '../config.js';
2
2
  import { ConnectionState, isJsonObject } from '../types.js';
3
+ import { validateGroupAIDFormat } from '../validators.js';
3
4
  import { hasGroupMentionModeField, normalizeGroupMentionMode } from './mention-mode.js';
4
5
  const PUSHED_SEQS_LIMIT = 50_000;
5
6
  const PENDING_ORDERED_LIMIT = 50_000;
6
7
  const MESSAGE_RECALL_SEEN_LIMIT = 10_000;
7
8
  const GROUP_RECALL_SEEN_LIMIT = 10_000;
8
9
  const SEQ_TRACKER_PERSIST_FLUSH_DELAY_MS = 200;
10
+ const DELIVERY_CHANGED_MESSAGE_EVENTS = new Set([
11
+ 'message.received',
12
+ 'group.message_created',
13
+ ]);
9
14
  const MAX_INLINE_PENDING_ACK_NAMESPACES = 4096;
10
15
  const APP_MESSAGE_ENVELOPE_KEYS = [
11
16
  'module_id', 'message_type', 'type', 'kind', 'version',
@@ -67,15 +72,33 @@ export class MessageDeliveryEngine {
67
72
  realtimeTailHeads = null;
68
73
  realtimeSyncing = null;
69
74
  pendingGroupPullUpper = null;
75
+ onlineUnreadHintTargets = null;
76
+ onlineUnreadHintTasks = null;
70
77
  pendingP2PInlineAcks = null;
71
78
  pendingGroupInlineAcks = null;
72
79
  pendingGroupInlineAckNamespaces = null;
73
80
  inlineAckGeneration = 0;
81
+ pendingDeliveryChanges = null;
82
+ deliveryChangedGeneration = 0;
74
83
  // 同一实例的 tracker 写入必须串行,避免旧的延迟 flush 覆盖新的 Forward 提交。
75
84
  seqTrackerWriteChain = Promise.resolve();
76
85
  constructor(runtime) {
77
86
  this.runtime = runtime;
78
87
  }
88
+ isPullOperationCurrent() {
89
+ const client = this.runtime.client;
90
+ const generation = client._pullOperationGeneration;
91
+ if (generation === undefined)
92
+ return true;
93
+ const pipeline = client._rpcPipeline;
94
+ return typeof pipeline?.isPullGenerationCurrent === 'function'
95
+ ? pipeline.isPullGenerationCurrent(generation)
96
+ : true;
97
+ }
98
+ ensurePullOperationCurrent() {
99
+ if (!this.isPullOperationCurrent())
100
+ throw new Error('pull invalidated');
101
+ }
79
102
  forwardCoordinator() {
80
103
  const coordinator = this.runtime.client._v2E2EE;
81
104
  if (!coordinator
@@ -91,6 +114,7 @@ export class MessageDeliveryEngine {
91
114
  }
92
115
  async confirmPlainForwardAck(ns, method, ackSeq, groupId = '') {
93
116
  const client = this.runtime.client;
117
+ this.ensurePullOperationCurrent();
94
118
  const coordinator = this.forwardCoordinator();
95
119
  const generation = this.captureInlineAckGeneration();
96
120
  coordinator.recordForwardAck(ns, ackSeq);
@@ -109,6 +133,7 @@ export class MessageDeliveryEngine {
109
133
  _rpc_background: true,
110
134
  };
111
135
  const result = await client._rpcPipeline.rawCall(method, params, { background: true });
136
+ this.ensurePullOperationCurrent();
112
137
  const actualAckSeq = this.resolveForwardAckSeq(result, ackSeq);
113
138
  if (actualAckSeq < ackSeq) {
114
139
  throw new Error(`${method} server ACK watermark ${actualAckSeq} is below requested ${ackSeq}`);
@@ -126,6 +151,7 @@ export class MessageDeliveryEngine {
126
151
  throw new Error(`${method} response must be an object`);
127
152
  }
128
153
  const client = this.runtime.client;
154
+ this.ensurePullOperationCurrent();
129
155
  const response = result;
130
156
  const messages = deduplicatePlainForwardPageMessages(response.messages, afterSeq);
131
157
  const coordinator = this.forwardCoordinator();
@@ -156,25 +182,30 @@ export class MessageDeliveryEngine {
156
182
  let committed = false;
157
183
  try {
158
184
  for (const rawMessage of messages) {
185
+ this.ensurePullOperationCurrent();
159
186
  const seq = positiveSafeSequenceHint(rawMessage.seq);
160
187
  if (method === 'message.pull') {
161
188
  const appEvent = this.p2pAppEventForMessage(rawMessage);
162
189
  if (await this.publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false)) {
163
190
  publishedCount += 1;
164
191
  }
192
+ this.ensurePullOperationCurrent();
165
193
  continue;
166
194
  }
167
195
  const message = normalizeGroupMentionMode(rawMessage);
168
196
  if (this.recallEventFromGroupMessage(message)) {
169
197
  if (await this.publishGroupRecallTombstone(groupId, seq, message)) {
198
+ this.ensurePullOperationCurrent();
170
199
  this.markPublishedSeq(ns, seq);
171
200
  publishedCount += 1;
172
201
  }
173
202
  }
174
203
  else if (await this.publishPulledMessage('group.message_created', ns, seq, message, false)) {
204
+ this.ensurePullOperationCurrent();
175
205
  publishedCount += 1;
176
206
  }
177
207
  }
208
+ this.ensurePullOperationCurrent();
178
209
  if (messages.length > 0)
179
210
  client._seqTracker.onPullResult(ns, messages, afterSeq);
180
211
  const commitTarget = Math.max(client._seqTracker.getContiguousSeq(ns), retentionFloor, visibilityFloor, deferredServerCursor);
@@ -182,18 +213,21 @@ export class MessageDeliveryEngine {
182
213
  client._seqTracker.forceContiguousSeq(ns, commitTarget);
183
214
  }
184
215
  if (client._seqTracker.getContiguousSeq(ns) !== pageContigBefore) {
185
- await this.drainOrderedMessages(ns, undefined, false, false);
216
+ this.ensurePullOperationCurrent();
217
+ await this.drainOrderedMessages(ns, undefined, false, false, 'pull');
218
+ this.ensurePullOperationCurrent();
186
219
  await client._commitSeqTrackerState(ns);
187
220
  }
188
221
  committed = true;
189
222
  }
190
223
  catch (exc) {
191
- if (!committed && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
224
+ if (this.isPullOperationCurrent() && !committed && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
192
225
  client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
193
226
  this.dropSeqTrackerPending(ns);
194
227
  }
195
228
  throw exc;
196
229
  }
230
+ this.ensurePullOperationCurrent();
197
231
  const committedAck = client._seqTracker.getContiguousSeq(ns);
198
232
  if (deferredServerCursor > 0 && committedAck >= deferredServerCursor) {
199
233
  coordinator.clearForwardCursor(ns, committedAck);
@@ -224,19 +258,132 @@ export class MessageDeliveryEngine {
224
258
  if (clampedAckSeq < pendingAckSeq) {
225
259
  throw new Error(`${ackMethod} cannot confirm pending Forward watermark ${pendingAckSeq}`);
226
260
  }
261
+ this.ensurePullOperationCurrent();
227
262
  await this.confirmPlainForwardAck(ns, ackMethod, pendingAckSeq, groupId);
228
263
  }
229
264
  return { rawCount: messages.length, publishedCount };
230
265
  }
231
266
  resetInlineAckState() {
267
+ this.deliveryChangedGeneration += 1;
268
+ this.pendingDeliveryChanges = null;
232
269
  this.inlineAckGeneration += 1;
270
+ void this.runtime.client._rpcPipeline?.invalidatePulls?.();
233
271
  this.pendingP2PInlineAcks = null;
234
272
  this.pendingGroupInlineAcks = null;
235
273
  this.pendingGroupInlineAckNamespaces = null;
236
274
  this.realtimeSyncing = null;
237
275
  this.pendingGroupPullUpper = null;
276
+ this.onlineUnreadHintTargets = null;
277
+ this.onlineUnreadHintTasks = null;
238
278
  this.realtimeTailHeads = null;
239
279
  }
280
+ deliveryChangedNamespace(event, payload) {
281
+ if (event === 'group.message_created' && isJsonObject(payload)) {
282
+ const aid = String(this.runtime.client._aid ?? '').trim();
283
+ const dot = aid.indexOf('.');
284
+ try {
285
+ const groupAid = validateGroupAIDFormat(payload.group_aid ?? payload.group_id, { localIssuer: dot >= 0 ? aid.slice(dot + 1) : '' });
286
+ return groupAid.includes('.') ? `group:${groupAid}` : '';
287
+ }
288
+ catch {
289
+ return '';
290
+ }
291
+ }
292
+ return 'p2p';
293
+ }
294
+ isDeliveryChangedMessage(event, payload) {
295
+ if (!DELIVERY_CHANGED_MESSAGE_EVENTS.has(event)
296
+ || !isJsonObject(payload))
297
+ return false;
298
+ const messageId = payload.message_id;
299
+ const seq = payload.seq;
300
+ return (typeof messageId === 'string' && messageId.trim().length > 0)
301
+ || (typeof seq === 'number' && Number.isSafeInteger(seq) && seq > 0);
302
+ }
303
+ deliveryChangedTrigger(source) {
304
+ if (source === 'inline-push')
305
+ return 'inline_push';
306
+ if (source === 'pull' || source === 'tail')
307
+ return 'pull_drained';
308
+ if (source === 'push' || source === 'group-push' || source === 'ordered')
309
+ return 'push';
310
+ return null;
311
+ }
312
+ noteDeliveryChanged(event, payload, source, context) {
313
+ if (!this.isDeliveryChangedMessage(event, payload))
314
+ return;
315
+ const trigger = this.deliveryChangedTrigger(source);
316
+ const namespace = this.deliveryChangedNamespace(event, payload);
317
+ if (!trigger || !namespace)
318
+ return;
319
+ const pending = trigger === 'pull_drained'
320
+ ? this.pendingDeliveryChanges ?? new Map()
321
+ : context?.changes ?? this.pendingDeliveryChanges ?? new Map();
322
+ if (trigger === 'pull_drained' || !context)
323
+ this.pendingDeliveryChanges = pending;
324
+ pending.set(namespace, (pending.get(namespace) ?? 0) + 1);
325
+ if (!context && trigger !== 'pull_drained')
326
+ this.flushDeliveryChanged(trigger);
327
+ }
328
+ flushDeliveryChanged(trigger) {
329
+ const pending = this.pendingDeliveryChanges;
330
+ if (!pending || pending.size === 0)
331
+ return;
332
+ const changes = [...pending.entries()]
333
+ .filter(([, deliveredCount]) => deliveredCount > 0)
334
+ .sort(([left], [right]) => left.localeCompare(right))
335
+ .map(([namespace, deliveredCount]) => ({ namespace, delivered_count: deliveredCount }));
336
+ pending.clear();
337
+ if (changes.length === 0)
338
+ return;
339
+ const payload = { trigger, changes };
340
+ const dispatcher = this.runtime.client._dispatcher;
341
+ const published = typeof dispatcher.enqueue === 'function'
342
+ ? (dispatcher.enqueue('delivery.changed', payload), undefined)
343
+ : typeof dispatcher.publishSyncAware === 'function'
344
+ ? dispatcher.publishSyncAware('delivery.changed', payload)
345
+ : dispatcher.publish?.('delivery.changed', payload);
346
+ if (isPromiseLike(published))
347
+ void Promise.resolve(published).catch((exc) => {
348
+ this.runtime.client._clientLog.warn(`delivery.changed handler failed: ${formatDeliveryError(exc)}`);
349
+ });
350
+ }
351
+ onPullDrained() {
352
+ this.flushDeliveryChanged('pull_drained');
353
+ }
354
+ flushDeliveryChangedContext(context, trigger) {
355
+ if (context.generation !== this.deliveryChangedGeneration || context.changes.size === 0)
356
+ return;
357
+ const pending = this.pendingDeliveryChanges;
358
+ if (pending) {
359
+ for (const [namespace, count] of pending) {
360
+ context.changes.set(namespace, (context.changes.get(namespace) ?? 0) + count);
361
+ }
362
+ pending.clear();
363
+ }
364
+ const changes = [...context.changes.entries()]
365
+ .filter(([, count]) => count > 0)
366
+ .sort(([left], [right]) => left.localeCompare(right))
367
+ .map(([namespace, delivered_count]) => ({ namespace, delivered_count }));
368
+ context.changes.clear();
369
+ if (changes.length === 0)
370
+ return;
371
+ const dispatcher = this.runtime.client._dispatcher;
372
+ const payload = { trigger, changes };
373
+ const published = typeof dispatcher.enqueue === 'function'
374
+ ? (dispatcher.enqueue('delivery.changed', payload), undefined)
375
+ : typeof dispatcher.publishSyncAware === 'function'
376
+ ? dispatcher.publishSyncAware('delivery.changed', payload)
377
+ : dispatcher.publish?.('delivery.changed', payload);
378
+ if (isPromiseLike(published))
379
+ void Promise.resolve(published).catch((exc) => {
380
+ this.runtime.client._clientLog.warn(`delivery.changed handler failed: ${formatDeliveryError(exc)}`);
381
+ });
382
+ }
383
+ invalidateDeliveryChanges() {
384
+ this.deliveryChangedGeneration += 1;
385
+ this.pendingDeliveryChanges = null;
386
+ }
240
387
  inlineGenerationIsCurrent(generation) {
241
388
  return this.inlineAckGeneration === generation;
242
389
  }
@@ -279,10 +426,17 @@ export class MessageDeliveryEngine {
279
426
  }
280
427
  endRealtimeSync(ns) {
281
428
  this.realtimeSyncing?.delete(ns);
429
+ this.runtime.client._rpcPipeline?.requestPullDrainedCheck?.();
282
430
  }
283
431
  hasPendingPull(ns) {
284
432
  return (this.pendingGroupPullUpper?.get(ns) ?? 0) > 0;
285
433
  }
434
+ hasAnyPendingPull() {
435
+ return [...(this.pendingGroupPullUpper?.values() ?? [])].some((seq) => seq > 0)
436
+ || Boolean(this.onlineUnreadHintTasks?.size)
437
+ || Boolean(this.realtimeSyncing?.size)
438
+ || Boolean(this.runtime.client._v2E2EE?.hasPendingSenderIKWork?.());
439
+ }
286
440
  onPullGateIdle(ns) {
287
441
  if (!ns)
288
442
  return;
@@ -347,14 +501,14 @@ export class MessageDeliveryEngine {
347
501
  client._pushedSeqs.set(ns, new Set(keep));
348
502
  }
349
503
  }
350
- enqueueOrderedMessage(ns, event, seq, payload) {
504
+ enqueueOrderedMessage(ns, event, seq, payload, source = 'push') {
351
505
  const client = this.runtime.client;
352
506
  let queue = client._pendingOrderedMsgs.get(ns);
353
507
  if (!queue) {
354
508
  queue = new Map();
355
509
  client._pendingOrderedMsgs.set(ns, queue);
356
510
  }
357
- queue.set(seq, { event, payload });
511
+ queue.set(seq, { event, payload, source });
358
512
  if (queue.size > PENDING_ORDERED_LIMIT) {
359
513
  const drop = [...queue.keys()].sort((a, b) => a - b).slice(0, queue.size - PENDING_ORDERED_LIMIT);
360
514
  for (const oldSeq of drop)
@@ -364,26 +518,30 @@ export class MessageDeliveryEngine {
364
518
  isGroupEventNamespace(ns) {
365
519
  return ns.startsWith('group_event:');
366
520
  }
367
- async publishOrderedQueueItem(ns, event, seq, payload, source, pullResponse = false) {
521
+ async publishOrderedQueueItem(ns, event, seq, payload, source, pullResponse = false, deliveryContext) {
368
522
  const client = this.runtime.client;
523
+ this.ensurePullOperationCurrent();
369
524
  if (event === 'group.changed' && this.isGroupEventNamespace(ns)) {
370
525
  await this.publishOrderedGroupChanged(payload, source);
526
+ this.ensurePullOperationCurrent();
371
527
  return;
372
528
  }
373
529
  if (event === 'message.recalled') {
374
530
  await this.publishMessageRecallTombstone(seq, payload);
531
+ this.ensurePullOperationCurrent();
375
532
  return;
376
533
  }
377
534
  if (pullResponse) {
378
- const published = client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, source));
535
+ const published = client._withPullResponseProcessing(ns, () => this.publishAppEvent(event, payload, source, deliveryContext));
379
536
  if (isPromiseLike(published))
380
537
  await published;
381
538
  }
382
539
  else {
383
- const published = client._publishAppEvent(event, payload, source);
540
+ const published = this.publishAppEvent(event, payload, source, deliveryContext);
384
541
  if (isPromiseLike(published))
385
542
  await published;
386
543
  }
544
+ this.ensurePullOperationCurrent();
387
545
  }
388
546
  async publishOrderedGroupChanged(payload, source = 'ordered') {
389
547
  const client = this.runtime.client;
@@ -899,7 +1057,7 @@ export class MessageDeliveryEngine {
899
1057
  if (contig !== contigBefore)
900
1058
  this.persistSeq(ns);
901
1059
  }
902
- publishAppEvent(event, payload, source = 'direct') {
1060
+ publishAppEvent(event, payload, source = 'direct', deliveryContext) {
903
1061
  const client = this.runtime.client;
904
1062
  if ((event === 'message.received' || event === 'group.message_created') && isJsonObject(payload)) {
905
1063
  client._maybeAppendEchoTraceReceive(payload);
@@ -919,7 +1077,23 @@ export class MessageDeliveryEngine {
919
1077
  client._clientLog.debug(`agent_md etag inject skipped: ${err instanceof Error ? err.message : String(err)}`);
920
1078
  }
921
1079
  }
922
- return client._dispatcher.publishSyncAware(event, this.normalizePublishedMessagePayload(event, payload));
1080
+ const generation = this.deliveryChangedGeneration;
1081
+ const normalized = this.normalizePublishedMessagePayload(event, payload);
1082
+ const dispatcher = client._dispatcher;
1083
+ const result = typeof dispatcher.enqueue === 'function'
1084
+ ? (dispatcher.enqueue(event, normalized), undefined)
1085
+ : typeof dispatcher.publishSyncAware === 'function'
1086
+ ? dispatcher.publishSyncAware(event, normalized)
1087
+ : dispatcher.publish?.(event, normalized);
1088
+ const note = () => {
1089
+ if (generation === this.deliveryChangedGeneration) {
1090
+ this.noteDeliveryChanged(event, payload, source, deliveryContext);
1091
+ }
1092
+ };
1093
+ if (isPromiseLike(result))
1094
+ return Promise.resolve(result).then(note);
1095
+ note();
1096
+ return result;
923
1097
  }
924
1098
  messagePayloadForDebug(message) {
925
1099
  if (!isJsonObject(message))
@@ -1938,6 +2112,18 @@ export class MessageDeliveryEngine {
1938
2112
  raw = response.effective_ack_seq;
1939
2113
  else if (Object.prototype.hasOwnProperty.call(response, 'ack_seq'))
1940
2114
  raw = response.ack_seq;
2115
+ else if (Object.prototype.hasOwnProperty.call(response, 'cursor')) {
2116
+ const cursor = response.cursor;
2117
+ const cursorObject = isJsonObject(cursor) ? cursor : null;
2118
+ if (cursorObject) {
2119
+ if (!Object.prototype.hasOwnProperty.call(cursorObject, 'current_seq'))
2120
+ return 0;
2121
+ raw = cursorObject.current_seq;
2122
+ }
2123
+ else {
2124
+ raw = cursor;
2125
+ }
2126
+ }
1941
2127
  else
1942
2128
  return requestedSeq;
1943
2129
  return typeof raw === 'number' && Number.isSafeInteger(raw) && raw >= 0 ? raw : 0;
@@ -2020,19 +2206,20 @@ export class MessageDeliveryEngine {
2020
2206
  const state = this.syncState(ns);
2021
2207
  const needsForward = state.ack < state.tail - 1;
2022
2208
  const backgroundEnabled = client._sessionOptions?.background_sync !== false;
2023
- // Tail 期间到达的新 Push 可能只更新 maxSeen 而没有扩展 Tail 窗口;
2024
- // 仍须在本次 Tail 周期内补一页 Forward,不能交给外层重复恢复。
2025
- const tailMissedDuringPull = tailHead !== null
2026
- && Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? 0) > state.head;
2209
+ // Tail 期间到达的新 Push 可能只更新 maxSeen 而没有扩展 Tail 窗口。
2210
+ const maxSeen = Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? 0);
2211
+ const tailMissedDuringPull = tailHead !== null && maxSeen > state.head;
2027
2212
  const tailForward = tailHead !== null && (needsForward || tailMissedDuringPull);
2028
2213
  const backgroundWindowForward = (order.length === 1 && order[0] === 'forward' && !headForward)
2029
2214
  || (order.length === 1 && order[0] === 'tail' && tailHead === null && needsForward);
2030
2215
  const foregroundForward = headForward || tailForward;
2031
2216
  if (foregroundForward || backgroundEnabled && backgroundWindowForward) {
2217
+ const forwardTarget = Math.max(headForward ? pushSeq : 0, tailForward || backgroundWindowForward ? state.tail - 1 : 0, tailMissedDuringPull ? maxSeen : 0);
2218
+ const requiredPages = Math.max(1, Math.ceil(Math.max(0, forwardTarget - state.ack) / pageLimit));
2032
2219
  result = await run({
2033
2220
  after_seq: state.ack,
2034
2221
  limit: pageLimit,
2035
- ...(headForward ? { max_pages: 2 } : order[0] === 'tail' ? { max_pages: 1 } : {}),
2222
+ ...(headForward ? { max_pages: Math.max(2, requiredPages) } : order[0] === 'tail' ? { max_pages: requiredPages } : {}),
2036
2223
  }, !foregroundForward, headForward || tailMissedDuringPull);
2037
2224
  }
2038
2225
  if (tailHead !== null && this.syncState(ns).ack < this.syncState(ns).tail - 1) {
@@ -2094,18 +2281,19 @@ export class MessageDeliveryEngine {
2094
2281
  const state = this.syncState(ns);
2095
2282
  const needsForward = state.ack < state.tail - 1;
2096
2283
  const backgroundEnabled = client._sessionOptions?.background_sync !== false;
2097
- // P2P 对称:Tail 期间出现的新 Push 在本次周期内补一页 Forward。
2098
- const tailMissedDuringPull = tailHead !== null
2099
- && Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? 0) > state.head;
2284
+ const maxSeen = Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? 0);
2285
+ const tailMissedDuringPull = tailHead !== null && maxSeen > state.head;
2100
2286
  const tailForward = tailHead !== null && (needsForward || tailMissedDuringPull);
2101
2287
  const backgroundWindowForward = (order.length === 1 && order[0] === 'forward' && !headForward)
2102
2288
  || (order.length === 1 && order[0] === 'tail' && tailHead === null && needsForward);
2103
2289
  const foregroundForward = headForward || tailForward;
2104
2290
  if (foregroundForward || backgroundEnabled && backgroundWindowForward) {
2291
+ const forwardTarget = Math.max(headForward ? pushSeq : 0, tailForward || backgroundWindowForward ? state.tail - 1 : 0, tailMissedDuringPull ? maxSeen : 0);
2292
+ const requiredPages = Math.max(1, Math.ceil(Math.max(0, forwardTarget - state.ack) / pageLimit));
2105
2293
  result = await run({
2106
2294
  after_seq: state.ack,
2107
2295
  limit: pageLimit,
2108
- ...(headForward ? { max_pages: 2 } : order[0] === 'tail' ? { max_pages: 1 } : {}),
2296
+ ...(headForward ? { max_pages: Math.max(2, requiredPages) } : order[0] === 'tail' ? { max_pages: requiredPages } : {}),
2109
2297
  }, !foregroundForward, headForward || tailMissedDuringPull);
2110
2298
  }
2111
2299
  if (tailHead !== null && this.syncState(ns).ack < this.syncState(ns).tail - 1) {
@@ -2129,12 +2317,14 @@ export class MessageDeliveryEngine {
2129
2317
  const key = client._rpcPipeline.pullGateKeyForCall('message.pull', request);
2130
2318
  return await client._rpcPipeline.runPullSerialized(key, invoke, background);
2131
2319
  }
2132
- async runGroupForwardRecovery(groupId, ns, pageLimit, background = false) {
2320
+ async runGroupForwardRecovery(groupId, ns, pageLimit, background = false, throughSeq = 0) {
2133
2321
  const client = this.runtime.client;
2134
- const request = { group_id: groupId, after_seq: this.syncState(ns).ack, limit: pageLimit, max_pages: 1 };
2322
+ const ack = this.syncState(ns).ack;
2323
+ const maxPages = Math.max(1, Math.ceil(Math.max(0, throughSeq - ack) / pageLimit));
2324
+ const request = { group_id: groupId, after_seq: ack, limit: pageLimit, max_pages: maxPages };
2135
2325
  const invoke = async () => {
2136
2326
  const after = this.syncState(ns).ack;
2137
- const messages = await client._pullGroupV2(groupId, after, pageLimit, { gateLocked: true, maxPages: 1 });
2327
+ const messages = await client._pullGroupV2(groupId, after, pageLimit, { gateLocked: true, maxPages });
2138
2328
  return { messages, raw_count: messages.length };
2139
2329
  };
2140
2330
  if (!client._rpcPipeline)
@@ -2770,9 +2960,35 @@ export class MessageDeliveryEngine {
2770
2960
  if (!groupId)
2771
2961
  return;
2772
2962
  const ns = `group:${groupId}`;
2773
- client._safeAsync(this.runGroupForwardRecovery(groupId, ns, 50, true).catch((exc) => {
2774
- client._clientLog.debug(`online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
2775
- }));
2963
+ const targets = this.onlineUnreadHintTargets ?? new Map();
2964
+ const tasks = this.onlineUnreadHintTasks ?? new Map();
2965
+ this.onlineUnreadHintTargets = targets;
2966
+ this.onlineUnreadHintTasks = tasks;
2967
+ targets.set(ns, Math.max(targets.get(ns) ?? 0, positiveSafeSequenceHint(data.seq)));
2968
+ if (tasks.has(ns))
2969
+ return;
2970
+ const task = Promise.resolve().then(async () => {
2971
+ try {
2972
+ while (true) {
2973
+ const ackBefore = this.syncState(ns).ack;
2974
+ await this.runGroupForwardRecovery(groupId, ns, 50, true, targets.get(ns) ?? 0);
2975
+ const ackAfter = this.syncState(ns).ack;
2976
+ if (ackAfter >= (targets.get(ns) ?? 0) || ackAfter <= ackBefore)
2977
+ return;
2978
+ }
2979
+ }
2980
+ catch (exc) {
2981
+ client._clientLog.debug(`online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
2982
+ }
2983
+ finally {
2984
+ targets.delete(ns);
2985
+ if (tasks.get(ns) === task)
2986
+ tasks.delete(ns);
2987
+ client._rpcPipeline?.requestPullDrainedCheck?.();
2988
+ }
2989
+ });
2990
+ tasks.set(ns, task);
2991
+ client._safeAsync(task);
2776
2992
  }
2777
2993
  async onRawGroupV2MessageCreated(data, expectedVersion, inlineGeneration) {
2778
2994
  const client = this.runtime.client;
@@ -2899,13 +3115,13 @@ export class MessageDeliveryEngine {
2899
3115
  catch (exc) {
2900
3116
  const error = formatDeliveryError(exc);
2901
3117
  client._clientLog.warn(`restore SeqTracker state failed: ${error}`);
2902
- client._dispatcher.publish('seq_tracker.persist_error', {
3118
+ client._dispatcher.enqueue('seq_tracker.persist_error', {
2903
3119
  phase: 'restore',
2904
3120
  aid: client._aid,
2905
3121
  device_id: client._deviceId,
2906
3122
  slot_id: client._slotId,
2907
3123
  error: String(error),
2908
- }).catch(() => { });
3124
+ });
2909
3125
  }
2910
3126
  }
2911
3127
  migrateSeqStateGroupIds(state) {
@@ -3124,13 +3340,13 @@ export class MessageDeliveryEngine {
3124
3340
  catch (exc) {
3125
3341
  const error = formatDeliveryError(exc);
3126
3342
  client._clientLog.warn(`save SeqTracker state failed: ${error}`);
3127
- client._dispatcher.publish('seq_tracker.persist_error', {
3343
+ client._dispatcher.enqueue('seq_tracker.persist_error', {
3128
3344
  phase: 'save',
3129
3345
  aid,
3130
3346
  device_id: deviceId,
3131
3347
  slot_id: slotId,
3132
3348
  error: String(error),
3133
- }).catch(() => { });
3349
+ });
3134
3350
  if (throwOnError)
3135
3351
  throw exc;
3136
3352
  }
@@ -3248,55 +3464,106 @@ export class MessageDeliveryEngine {
3248
3464
  }
3249
3465
  return params;
3250
3466
  }
3251
- async drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true) {
3467
+ async drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true, source = 'push', context) {
3252
3468
  const client = this.runtime.client;
3469
+ this.ensurePullOperationCurrent();
3253
3470
  const queue = client._pendingOrderedMsgs.get(ns);
3254
3471
  if (!queue || queue.size === 0)
3255
3472
  return;
3256
3473
  let delivered = false;
3257
- while (true) {
3258
- const contig = client._seqTracker.getContiguousSeq(ns);
3259
- const ready = [...queue.keys()]
3260
- .filter((seq) => seq <= contig && (beforeSeq === undefined || seq < beforeSeq))
3261
- .sort((a, b) => a - b);
3262
- let seq = ready[0];
3263
- if (seq === undefined) {
3264
- const nextSeq = contig + 1;
3265
- if (beforeSeq !== undefined && nextSeq >= beforeSeq)
3266
- break;
3267
- if (!queue.has(nextSeq))
3268
- break;
3269
- seq = nextSeq;
3270
- }
3271
- if (seq === undefined)
3272
- continue;
3273
- const item = queue.get(seq);
3274
- queue.delete(seq);
3275
- if (!item)
3276
- continue;
3277
- if (client._pushedSeqs.get(ns)?.has(seq)) {
3278
- client._clientLog.debug(`publish ordered drain skipped duplicate: ns=${ns}, seq=${seq}, event=${item.event}`);
3474
+ const rootTrigger = this.deliveryChangedTrigger(source);
3475
+ const drainContexts = new Map();
3476
+ try {
3477
+ while (true) {
3478
+ this.ensurePullOperationCurrent();
3479
+ const contig = client._seqTracker.getContiguousSeq(ns);
3480
+ const ready = [...queue.keys()]
3481
+ .filter((seq) => seq <= contig && (beforeSeq === undefined || seq < beforeSeq))
3482
+ .sort((a, b) => a - b);
3483
+ let seq = ready[0];
3484
+ if (seq === undefined) {
3485
+ const nextSeq = contig + 1;
3486
+ if (beforeSeq !== undefined && nextSeq >= beforeSeq)
3487
+ break;
3488
+ if (!queue.has(nextSeq))
3489
+ break;
3490
+ seq = nextSeq;
3491
+ }
3492
+ if (seq === undefined)
3493
+ continue;
3494
+ const item = queue.get(seq);
3495
+ queue.delete(seq);
3496
+ if (!item)
3497
+ continue;
3498
+ if (client._pushedSeqs.get(ns)?.has(seq)) {
3499
+ client._clientLog.debug(`publish ordered drain skipped duplicate: ns=${ns}, seq=${seq}, event=${item.event}`);
3500
+ client._markOrderedSeqDelivered(ns, seq);
3501
+ continue;
3502
+ }
3503
+ const itemSource = item.source ?? source;
3504
+ const itemTrigger = this.deliveryChangedTrigger(itemSource);
3505
+ let itemContext = itemTrigger === rootTrigger ? context : undefined;
3506
+ if (itemTrigger === 'push' || itemTrigger === 'inline_push') {
3507
+ if (!itemContext) {
3508
+ itemContext = drainContexts.get(itemTrigger);
3509
+ if (!itemContext) {
3510
+ itemContext = { generation: this.deliveryChangedGeneration, changes: new Map() };
3511
+ drainContexts.set(itemTrigger, itemContext);
3512
+ }
3513
+ }
3514
+ }
3515
+ await this.publishOrderedQueueItem(ns, item.event, seq, item.payload, itemSource, pullResponse, itemContext);
3516
+ this.ensurePullOperationCurrent();
3517
+ this.markPublishedSeq(ns, seq);
3279
3518
  client._markOrderedSeqDelivered(ns, seq);
3280
- continue;
3519
+ delivered = true;
3520
+ client._clientLog.debug(`publish ordered drain delivered: ns=${ns}, seq=${seq}, event=${item.event}`);
3521
+ }
3522
+ }
3523
+ finally {
3524
+ for (const [trigger, drainContext] of drainContexts) {
3525
+ if (drainContext !== context)
3526
+ this.flushDeliveryChangedContext(drainContext, trigger);
3281
3527
  }
3282
- await this.publishOrderedQueueItem(ns, item.event, seq, item.payload, 'ordered-drain', pullResponse);
3283
- this.markPublishedSeq(ns, seq);
3284
- client._markOrderedSeqDelivered(ns, seq);
3285
- delivered = true;
3286
- client._clientLog.debug(`publish ordered drain delivered: ns=${ns}, seq=${seq}, event=${item.event}`);
3287
3528
  }
3288
3529
  if (queue.size === 0) {
3289
3530
  client._pendingOrderedMsgs.delete(ns);
3290
- if (delivered && persist)
3531
+ if (delivered && persist) {
3532
+ this.ensurePullOperationCurrent();
3291
3533
  await this.saveSeqTrackerState();
3534
+ }
3292
3535
  }
3293
3536
  }
3294
- async publishOrderedMessage(event, ns, seq, payload) {
3537
+ async publishOrderedMessage(event, ns, seq, payload, source = 'push') {
3538
+ const context = {
3539
+ generation: this.deliveryChangedGeneration,
3540
+ changes: new Map(),
3541
+ };
3542
+ try {
3543
+ return await this.publishOrderedMessageInternal(event, ns, seq, payload, source, context);
3544
+ }
3545
+ finally {
3546
+ const trigger = this.deliveryChangedTrigger(source);
3547
+ if (trigger === 'pull_drained' && context.generation === this.deliveryChangedGeneration) {
3548
+ const pending = this.pendingDeliveryChanges ?? new Map();
3549
+ this.pendingDeliveryChanges = pending;
3550
+ for (const [namespace, count] of context.changes) {
3551
+ pending.set(namespace, (pending.get(namespace) ?? 0) + count);
3552
+ }
3553
+ }
3554
+ else if (trigger && trigger !== 'pull_drained') {
3555
+ this.flushDeliveryChangedContext(context, trigger);
3556
+ }
3557
+ }
3558
+ }
3559
+ async publishOrderedMessageInternal(event, ns, seq, payload, source, context) {
3295
3560
  const client = this.runtime.client;
3561
+ this.ensurePullOperationCurrent();
3296
3562
  const seqNum = Number(seq);
3297
3563
  if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0) {
3298
3564
  client._clientLog.debug(`publish ordered direct(no-seq): event=${event}, ns=${ns || '<none>'}, seq=${String(seq)}`);
3299
- await this.publishOrderedQueueItem(ns, event, seqNum, payload, 'ordered');
3565
+ await this.publishOrderedQueueItem(ns, event, seqNum, payload, source, false, context);
3566
+ this.ensurePullOperationCurrent();
3300
3567
  return true;
3301
3568
  }
3302
3569
  if (client._pushedSeqs.get(ns)?.has(seqNum)) {
@@ -3310,10 +3577,11 @@ export class MessageDeliveryEngine {
3310
3577
  const contig = client._seqTracker.getContiguousSeq(ns);
3311
3578
  if (seqNum > contig && seqNum !== contig + 1) {
3312
3579
  client._clientLog.debug(`publish ordered enqueue(gap): event=${event}, ns=${ns}, seq=${seqNum}, contiguous=${contig}`);
3313
- this.enqueueOrderedMessage(ns, event, seqNum, payload);
3580
+ this.enqueueOrderedMessage(ns, event, seqNum, payload, source);
3314
3581
  return false;
3315
3582
  }
3316
- await this.drainOrderedMessages(ns, seqNum);
3583
+ await this.drainOrderedMessages(ns, seqNum, false, true, source, context);
3584
+ this.ensurePullOperationCurrent();
3317
3585
  if (client._pushedSeqs.get(ns)?.has(seqNum)) {
3318
3586
  client._clientLog.debug(`publish ordered skipped after-drain duplicate: event=${event}, ns=${ns}, seq=${seqNum}`);
3319
3587
  return false;
@@ -3322,20 +3590,25 @@ export class MessageDeliveryEngine {
3322
3590
  queue?.delete(seqNum);
3323
3591
  if (queue && queue.size === 0)
3324
3592
  client._pendingOrderedMsgs.delete(ns);
3325
- await this.publishOrderedQueueItem(ns, event, seqNum, payload, 'ordered');
3593
+ await this.publishOrderedQueueItem(ns, event, seqNum, payload, source, false, context);
3594
+ this.ensurePullOperationCurrent();
3326
3595
  this.markPublishedSeq(ns, seqNum);
3327
3596
  client._markOrderedSeqDelivered(ns, seqNum);
3328
3597
  client._clientLog.debug(`publish ordered delivered: event=${event}, ns=${ns}, seq=${seqNum}`);
3329
- await this.drainOrderedMessages(ns);
3330
- if (!client._pendingOrderedMsgs.get(ns))
3598
+ await this.drainOrderedMessages(ns, undefined, false, true, source, context);
3599
+ if (!client._pendingOrderedMsgs.get(ns)) {
3600
+ this.ensurePullOperationCurrent();
3331
3601
  await this.saveSeqTrackerState();
3602
+ }
3332
3603
  return true;
3333
3604
  }
3334
3605
  async publishOrderedGroupRecall(ns, seq, message) {
3335
3606
  const client = this.runtime.client;
3607
+ this.ensurePullOperationCurrent();
3336
3608
  const seqNum = Number(seq);
3337
3609
  if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0) {
3338
3610
  await this.publishGroupRecallTombstone(ns.replace(/^group:/, ''), seq, message);
3611
+ this.ensurePullOperationCurrent();
3339
3612
  return true;
3340
3613
  }
3341
3614
  // 撤回 tombstone 也占 seq:推进 contiguous 并标记已发布,确保 ack 正常推进、不留空洞。
@@ -3345,24 +3618,31 @@ export class MessageDeliveryEngine {
3345
3618
  }
3346
3619
  client._seqTracker.onMessageSeq(ns, seqNum);
3347
3620
  await this.publishGroupRecallTombstone(ns.replace(/^group:/, ''), seq, message);
3621
+ this.ensurePullOperationCurrent();
3348
3622
  this.markPublishedSeq(ns, seqNum);
3349
3623
  client._markOrderedSeqDelivered?.(ns, seqNum);
3350
3624
  await this.drainOrderedMessages(ns);
3351
- if (!client._pendingOrderedMsgs.get(ns))
3625
+ if (!client._pendingOrderedMsgs.get(ns)) {
3626
+ this.ensurePullOperationCurrent();
3352
3627
  await this.saveSeqTrackerState();
3628
+ }
3353
3629
  return true;
3354
3630
  }
3355
3631
  async publishPulledMessage(event, ns, seq, payload, persist = true) {
3356
3632
  const client = this.runtime.client;
3633
+ this.ensurePullOperationCurrent();
3357
3634
  const seqNum = Number(seq);
3358
3635
  if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0 || !ns) {
3359
3636
  client._clientLog.debug(`publish pulled direct(no-seq): event=${event}, ns=${ns || '<none>'}, seq=${String(seq)}`);
3360
3637
  if (event === 'message.recalled') {
3361
- return this.publishMessageRecallTombstone(seq, payload);
3638
+ const published = await this.publishMessageRecallTombstone(seq, payload);
3639
+ this.ensurePullOperationCurrent();
3640
+ return published;
3362
3641
  }
3363
3642
  const published = client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, 'pull'));
3364
3643
  if (isPromiseLike(published))
3365
3644
  await published;
3645
+ this.ensurePullOperationCurrent();
3366
3646
  return true;
3367
3647
  }
3368
3648
  const queue = client._pendingOrderedMsgs.get(ns);
@@ -3373,23 +3653,28 @@ export class MessageDeliveryEngine {
3373
3653
  client._pendingOrderedMsgs.delete(ns);
3374
3654
  return false;
3375
3655
  }
3376
- await this.drainOrderedMessages(ns, seqNum, true, persist);
3656
+ await this.drainOrderedMessages(ns, seqNum, true, persist, 'pull');
3657
+ this.ensurePullOperationCurrent();
3377
3658
  queue?.delete(seqNum);
3378
3659
  if (queue && queue.size === 0)
3379
3660
  client._pendingOrderedMsgs.delete(ns);
3380
3661
  if (event === 'message.recalled') {
3381
3662
  const recallPublished = await this.publishMessageRecallTombstone(seqNum, payload);
3663
+ this.ensurePullOperationCurrent();
3382
3664
  this.markPublishedSeq(ns, seqNum);
3383
3665
  client._clientLog.debug(`publish pulled delivered: event=${event}, ns=${ns}, seq=${seqNum}`);
3384
- await this.drainOrderedMessages(ns, undefined, true, persist);
3666
+ await this.drainOrderedMessages(ns, undefined, true, persist, 'pull');
3667
+ this.ensurePullOperationCurrent();
3385
3668
  return recallPublished;
3386
3669
  }
3387
3670
  const published = client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, 'pull'));
3388
3671
  if (isPromiseLike(published))
3389
3672
  await published;
3673
+ this.ensurePullOperationCurrent();
3390
3674
  this.markPublishedSeq(ns, seqNum);
3391
3675
  client._clientLog.debug(`publish pulled delivered: event=${event}, ns=${ns}, seq=${seqNum}`);
3392
- await this.drainOrderedMessages(ns, undefined, true, persist);
3676
+ await this.drainOrderedMessages(ns, undefined, true, persist, 'pull');
3677
+ this.ensurePullOperationCurrent();
3393
3678
  return true;
3394
3679
  }
3395
3680
  }