@agentunion/fastaun 0.5.9 → 0.5.11

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 (43) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/_packed_docs/CHANGELOG.md +64 -0
  3. package/_packed_docs/INDEX.md +3 -3
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +1 -1
  5. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +6 -5
  6. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +102 -8
  7. package/_packed_docs/sdk/09-group-rpc-manual.md +18 -3
  8. package/_packed_docs/sdk/09-message-rpc-manual.md +34 -10
  9. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +3 -2
  10. package/_packed_docs/sdk/INDEX.md +2 -1
  11. package/dist/agent-md.js +5 -1
  12. package/dist/agent-md.js.map +1 -1
  13. package/dist/auth.d.ts +2 -1
  14. package/dist/auth.js +30 -44
  15. package/dist/auth.js.map +1 -1
  16. package/dist/client/delivery.d.ts +39 -9
  17. package/dist/client/delivery.js +407 -73
  18. package/dist/client/delivery.js.map +1 -1
  19. package/dist/client/group-state.js +6 -6
  20. package/dist/client/group-state.js.map +1 -1
  21. package/dist/client/lifecycle.js +5 -14
  22. package/dist/client/lifecycle.js.map +1 -1
  23. package/dist/client/rpc-pipeline.d.ts +4 -0
  24. package/dist/client/rpc-pipeline.js +51 -5
  25. package/dist/client/rpc-pipeline.js.map +1 -1
  26. package/dist/client/v2-e2ee.d.ts +3 -0
  27. package/dist/client/v2-e2ee.js +236 -59
  28. package/dist/client/v2-e2ee.js.map +1 -1
  29. package/dist/client.d.ts +1 -0
  30. package/dist/client.js +84 -30
  31. package/dist/client.js.map +1 -1
  32. package/dist/events.d.ts +23 -8
  33. package/dist/events.js +120 -26
  34. package/dist/events.js.map +1 -1
  35. package/dist/register-flow.js +15 -111
  36. package/dist/register-flow.js.map +1 -1
  37. package/dist/transport.d.ts +36 -3
  38. package/dist/transport.js +855 -33
  39. package/dist/transport.js.map +1 -1
  40. package/dist/version.d.ts +1 -1
  41. package/dist/version.js +1 -1
  42. package/dist/version.js.map +1 -1
  43. package/package.json +1 -1
@@ -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',
@@ -73,6 +78,10 @@ export class MessageDeliveryEngine {
73
78
  pendingGroupInlineAcks = null;
74
79
  pendingGroupInlineAckNamespaces = null;
75
80
  inlineAckGeneration = 0;
81
+ pendingDeliveryChanges = null;
82
+ deliveryChangedGeneration = 0;
83
+ syncRun = 0;
84
+ syncSessions = null;
76
85
  // 同一实例的 tracker 写入必须串行,避免旧的延迟 flush 覆盖新的 Forward 提交。
77
86
  seqTrackerWriteChain = Promise.resolve();
78
87
  constructor(runtime) {
@@ -145,6 +154,7 @@ export class MessageDeliveryEngine {
145
154
  }
146
155
  const client = this.runtime.client;
147
156
  this.ensurePullOperationCurrent();
157
+ this.syncStarted(ns);
148
158
  const response = result;
149
159
  const messages = deduplicatePlainForwardPageMessages(response.messages, afterSeq);
150
160
  const coordinator = this.forwardCoordinator();
@@ -187,7 +197,7 @@ export class MessageDeliveryEngine {
187
197
  }
188
198
  const message = normalizeGroupMentionMode(rawMessage);
189
199
  if (this.recallEventFromGroupMessage(message)) {
190
- if (await this.publishGroupRecallTombstone(groupId, seq, message)) {
200
+ if (await this.publishGroupRecallTombstone(groupId, seq, message, 'pull')) {
191
201
  this.ensurePullOperationCurrent();
192
202
  this.markPublishedSeq(ns, seq);
193
203
  publishedCount += 1;
@@ -207,7 +217,7 @@ export class MessageDeliveryEngine {
207
217
  }
208
218
  if (client._seqTracker.getContiguousSeq(ns) !== pageContigBefore) {
209
219
  this.ensurePullOperationCurrent();
210
- await this.drainOrderedMessages(ns, undefined, false, false);
220
+ await this.drainOrderedMessages(ns, undefined, false, false, 'pull');
211
221
  this.ensurePullOperationCurrent();
212
222
  await client._commitSeqTrackerState(ns);
213
223
  }
@@ -254,9 +264,19 @@ export class MessageDeliveryEngine {
254
264
  this.ensurePullOperationCurrent();
255
265
  await this.confirmPlainForwardAck(ns, ackMethod, pendingAckSeq, groupId);
256
266
  }
267
+ this.syncProgress(ns, {
268
+ rawCount: messages.length,
269
+ pulledCount: publishedCount,
270
+ remaining: response.remaining,
271
+ hasMore: typeof response.has_more === 'boolean' ? response.has_more : undefined,
272
+ currentSeq: client._seqTracker.getContiguousSeq(ns),
273
+ });
257
274
  return { rawCount: messages.length, publishedCount };
258
275
  }
259
276
  resetInlineAckState() {
277
+ this.syncStopped('aborted');
278
+ this.deliveryChangedGeneration += 1;
279
+ this.pendingDeliveryChanges = null;
260
280
  this.inlineAckGeneration += 1;
261
281
  void this.runtime.client._rpcPipeline?.invalidatePulls?.();
262
282
  this.pendingP2PInlineAcks = null;
@@ -267,6 +287,225 @@ export class MessageDeliveryEngine {
267
287
  this.onlineUnreadHintTargets = null;
268
288
  this.onlineUnreadHintTasks = null;
269
289
  this.realtimeTailHeads = null;
290
+ this.syncSessions = null;
291
+ }
292
+ deliveryChangedNamespace(event, payload) {
293
+ if (event === 'group.message_created' && isJsonObject(payload)) {
294
+ const aid = String(this.runtime.client._aid ?? '').trim();
295
+ const dot = aid.indexOf('.');
296
+ try {
297
+ const groupAid = validateGroupAIDFormat(payload.group_aid ?? payload.group_id, { localIssuer: dot >= 0 ? aid.slice(dot + 1) : '' });
298
+ return groupAid.includes('.') ? `group:${groupAid}` : '';
299
+ }
300
+ catch {
301
+ return '';
302
+ }
303
+ }
304
+ return 'p2p';
305
+ }
306
+ isDeliveryChangedMessage(event, payload) {
307
+ if (!DELIVERY_CHANGED_MESSAGE_EVENTS.has(event)
308
+ || !isJsonObject(payload))
309
+ return false;
310
+ const messageId = payload.message_id;
311
+ const seq = payload.seq;
312
+ return (typeof messageId === 'string' && messageId.trim().length > 0)
313
+ || (typeof seq === 'number' && Number.isSafeInteger(seq) && seq > 0);
314
+ }
315
+ deliveryChangedTrigger(source) {
316
+ if (source === 'inline-push' || source === 'inline_push')
317
+ return 'inline_push';
318
+ if (source === 'pull' || source === 'tail' || source === 'pending-retry' || source === 'pending_retry')
319
+ return 'pull_drained';
320
+ if (source === 'push' || source === 'group-push' || source === 'ordered')
321
+ return 'push';
322
+ return null;
323
+ }
324
+ noteDeliveryChanged(event, payload, source, context) {
325
+ if (!this.isDeliveryChangedMessage(event, payload))
326
+ return;
327
+ const trigger = this.deliveryChangedTrigger(source);
328
+ const namespace = this.deliveryChangedNamespace(event, payload);
329
+ if (!trigger || !namespace)
330
+ return;
331
+ const pending = trigger === 'pull_drained'
332
+ ? this.pendingDeliveryChanges ?? new Map()
333
+ : context?.changes ?? this.pendingDeliveryChanges ?? new Map();
334
+ if (trigger === 'pull_drained' || !context)
335
+ this.pendingDeliveryChanges = pending;
336
+ pending.set(namespace, (pending.get(namespace) ?? 0) + 1);
337
+ if (!context && trigger !== 'pull_drained')
338
+ this.flushDeliveryChanged(trigger);
339
+ }
340
+ flushDeliveryChanged(trigger) {
341
+ const pending = this.pendingDeliveryChanges;
342
+ if (!pending || pending.size === 0)
343
+ return;
344
+ const changes = [...pending.entries()]
345
+ .filter(([, deliveredCount]) => deliveredCount > 0)
346
+ .sort(([left], [right]) => left.localeCompare(right))
347
+ .map(([namespace, deliveredCount]) => ({ namespace, delivered_count: deliveredCount }));
348
+ pending.clear();
349
+ if (changes.length === 0)
350
+ return;
351
+ const payload = { source: trigger === 'pull_drained' ? 'pull' : trigger, trigger, changes };
352
+ const dispatcher = this.runtime.client._dispatcher;
353
+ const published = typeof dispatcher.enqueue === 'function'
354
+ ? (dispatcher.enqueue('delivery.changed', payload), undefined)
355
+ : typeof dispatcher.publishSyncAware === 'function'
356
+ ? dispatcher.publishSyncAware('delivery.changed', payload)
357
+ : dispatcher.publish?.('delivery.changed', payload);
358
+ if (isPromiseLike(published))
359
+ void Promise.resolve(published).catch((exc) => {
360
+ this.runtime.client._clientLog.warn(`delivery.changed handler failed: ${formatDeliveryError(exc)}`);
361
+ });
362
+ }
363
+ publishSyncLifecycle(event, payload) {
364
+ const dispatcher = this.runtime.client._dispatcher;
365
+ const published = typeof dispatcher.enqueue === 'function'
366
+ ? (dispatcher.enqueue(event, payload), undefined)
367
+ : typeof dispatcher.publishSyncAware === 'function'
368
+ ? dispatcher.publishSyncAware(event, payload)
369
+ : dispatcher.publish?.(event, payload);
370
+ if (isPromiseLike(published))
371
+ void Promise.resolve(published).catch((exc) => {
372
+ this.runtime.client._clientLog.warn(`${event} handler failed: ${formatDeliveryError(exc)}`);
373
+ });
374
+ }
375
+ syncStarted(ns) {
376
+ const namespace = String(ns ?? '').trim();
377
+ if (!namespace)
378
+ return;
379
+ const sessions = this.syncSessions ?? new Map();
380
+ this.syncSessions = sessions;
381
+ if (sessions.has(namespace))
382
+ return;
383
+ if (sessions.size === 0) {
384
+ this.syncRun += 1;
385
+ this.publishSyncLifecycle('sync.started', {
386
+ run_id: this.syncRun,
387
+ source: 'pull',
388
+ syncing: true,
389
+ namespaces_pending: 1,
390
+ received_total: 0,
391
+ namespace,
392
+ started_at: Date.now(),
393
+ });
394
+ }
395
+ const session = { pages: 0, pulled: 0, startedAt: Date.now() };
396
+ sessions.set(namespace, session);
397
+ }
398
+ syncProgress(ns, progress = {}) {
399
+ const namespace = String(ns ?? '').trim();
400
+ if (!namespace)
401
+ return;
402
+ this.syncStarted(namespace);
403
+ const session = this.syncSessions?.get(namespace);
404
+ if (!session)
405
+ return;
406
+ const pulledCount = Math.max(0, Number(progress.pulledCount ?? progress.rawCount ?? 0) || 0);
407
+ const rawCount = Math.max(0, Number(progress.rawCount ?? pulledCount) || 0);
408
+ session.pages += 1;
409
+ session.pulled += pulledCount;
410
+ const payload = {
411
+ run_id: this.syncRun,
412
+ source: 'pull',
413
+ namespace,
414
+ page: session.pages,
415
+ page_pulled_count: pulledCount,
416
+ page_raw_count: rawCount,
417
+ pulled_count: session.pulled,
418
+ received_total: session.pulled,
419
+ batch_size: pulledCount,
420
+ };
421
+ if (typeof progress.remaining === 'number'
422
+ && Number.isSafeInteger(progress.remaining) && progress.remaining >= 0) {
423
+ payload.remaining = progress.remaining;
424
+ payload.estimated_remaining = payload.remaining;
425
+ }
426
+ if (progress.hasMore !== undefined) {
427
+ payload.has_more = Boolean(progress.hasMore);
428
+ if (progress.remaining === undefined || progress.remaining === null) {
429
+ payload.estimated_remaining = progress.hasMore ? undefined : 0;
430
+ }
431
+ }
432
+ if (progress.currentSeq !== undefined && Number.isFinite(Number(progress.currentSeq))) {
433
+ payload.current_seq = Math.max(0, Number(progress.currentSeq));
434
+ }
435
+ if (progress.targetSeq !== undefined && Number.isFinite(Number(progress.targetSeq))) {
436
+ payload.target_seq = Math.max(0, Number(progress.targetSeq));
437
+ }
438
+ this.publishSyncLifecycle('sync.progress', payload);
439
+ }
440
+ onPullDrained() {
441
+ this.flushDeliveryChanged('pull_drained');
442
+ this.syncStopped('pull_drained');
443
+ }
444
+ onPullAborted() {
445
+ this.syncStopped('aborted');
446
+ }
447
+ syncStopped(reason = 'completed') {
448
+ const sessions = this.syncSessions;
449
+ if (!sessions || sessions.size === 0)
450
+ return;
451
+ const entries = [...sessions.entries()];
452
+ const receivedTotal = entries.reduce((total, [, session]) => total + session.pulled, 0);
453
+ const payload = {
454
+ run_id: this.syncRun,
455
+ source: 'pull',
456
+ syncing: false,
457
+ namespaces_pending: 0,
458
+ received_total: receivedTotal,
459
+ reason,
460
+ stopped_at: Date.now(),
461
+ };
462
+ if (entries.length === 1) {
463
+ const [namespace, session] = entries[0];
464
+ payload.namespace = namespace;
465
+ payload.pages = session.pages;
466
+ payload.pulled_count = session.pulled;
467
+ }
468
+ else {
469
+ payload.namespaces = entries.map(([namespace]) => namespace);
470
+ payload.pages = entries.reduce((total, [, session]) => total + session.pages, 0);
471
+ payload.pulled_count = receivedTotal;
472
+ }
473
+ sessions.clear();
474
+ this.syncSessions = null;
475
+ this.publishSyncLifecycle('sync.stopped', payload);
476
+ }
477
+ flushDeliveryChangedContext(context, trigger) {
478
+ if (context.generation !== this.deliveryChangedGeneration || context.changes.size === 0)
479
+ return;
480
+ const pending = this.pendingDeliveryChanges;
481
+ if (pending) {
482
+ for (const [namespace, count] of pending) {
483
+ context.changes.set(namespace, (context.changes.get(namespace) ?? 0) + count);
484
+ }
485
+ pending.clear();
486
+ }
487
+ const changes = [...context.changes.entries()]
488
+ .filter(([, count]) => count > 0)
489
+ .sort(([left], [right]) => left.localeCompare(right))
490
+ .map(([namespace, delivered_count]) => ({ namespace, delivered_count }));
491
+ context.changes.clear();
492
+ if (changes.length === 0)
493
+ return;
494
+ const dispatcher = this.runtime.client._dispatcher;
495
+ const payload = { trigger, source: trigger, changes };
496
+ const published = typeof dispatcher.enqueue === 'function'
497
+ ? (dispatcher.enqueue('delivery.changed', payload), undefined)
498
+ : typeof dispatcher.publishSyncAware === 'function'
499
+ ? dispatcher.publishSyncAware('delivery.changed', payload)
500
+ : dispatcher.publish?.('delivery.changed', payload);
501
+ if (isPromiseLike(published))
502
+ void Promise.resolve(published).catch((exc) => {
503
+ this.runtime.client._clientLog.warn(`delivery.changed handler failed: ${formatDeliveryError(exc)}`);
504
+ });
505
+ }
506
+ invalidateDeliveryChanges() {
507
+ this.deliveryChangedGeneration += 1;
508
+ this.pendingDeliveryChanges = null;
270
509
  }
271
510
  inlineGenerationIsCurrent(generation) {
272
511
  return this.inlineAckGeneration === generation;
@@ -310,10 +549,17 @@ export class MessageDeliveryEngine {
310
549
  }
311
550
  endRealtimeSync(ns) {
312
551
  this.realtimeSyncing?.delete(ns);
552
+ this.runtime.client._rpcPipeline?.requestPullDrainedCheck?.();
313
553
  }
314
554
  hasPendingPull(ns) {
315
555
  return (this.pendingGroupPullUpper?.get(ns) ?? 0) > 0;
316
556
  }
557
+ hasAnyPendingPull() {
558
+ return [...(this.pendingGroupPullUpper?.values() ?? [])].some((seq) => seq > 0)
559
+ || Boolean(this.onlineUnreadHintTasks?.size)
560
+ || Boolean(this.realtimeSyncing?.size)
561
+ || Boolean(this.runtime.client._v2E2EE?.hasPendingSenderIKWork?.());
562
+ }
317
563
  onPullGateIdle(ns) {
318
564
  if (!ns)
319
565
  return;
@@ -378,14 +624,14 @@ export class MessageDeliveryEngine {
378
624
  client._pushedSeqs.set(ns, new Set(keep));
379
625
  }
380
626
  }
381
- enqueueOrderedMessage(ns, event, seq, payload) {
627
+ enqueueOrderedMessage(ns, event, seq, payload, source = 'push') {
382
628
  const client = this.runtime.client;
383
629
  let queue = client._pendingOrderedMsgs.get(ns);
384
630
  if (!queue) {
385
631
  queue = new Map();
386
632
  client._pendingOrderedMsgs.set(ns, queue);
387
633
  }
388
- queue.set(seq, { event, payload });
634
+ queue.set(seq, { event, payload, source });
389
635
  if (queue.size > PENDING_ORDERED_LIMIT) {
390
636
  const drop = [...queue.keys()].sort((a, b) => a - b).slice(0, queue.size - PENDING_ORDERED_LIMIT);
391
637
  for (const oldSeq of drop)
@@ -395,7 +641,7 @@ export class MessageDeliveryEngine {
395
641
  isGroupEventNamespace(ns) {
396
642
  return ns.startsWith('group_event:');
397
643
  }
398
- async publishOrderedQueueItem(ns, event, seq, payload, source, pullResponse = false) {
644
+ async publishOrderedQueueItem(ns, event, seq, payload, source, pullResponse = false, deliveryContext) {
399
645
  const client = this.runtime.client;
400
646
  this.ensurePullOperationCurrent();
401
647
  if (event === 'group.changed' && this.isGroupEventNamespace(ns)) {
@@ -404,17 +650,17 @@ export class MessageDeliveryEngine {
404
650
  return;
405
651
  }
406
652
  if (event === 'message.recalled') {
407
- await this.publishMessageRecallTombstone(seq, payload);
653
+ await this.publishMessageRecallTombstone(seq, payload, source);
408
654
  this.ensurePullOperationCurrent();
409
655
  return;
410
656
  }
411
657
  if (pullResponse) {
412
- const published = client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, source));
658
+ const published = client._withPullResponseProcessing(ns, () => this.publishAppEvent(event, payload, source, deliveryContext));
413
659
  if (isPromiseLike(published))
414
660
  await published;
415
661
  }
416
662
  else {
417
- const published = client._publishAppEvent(event, payload, source);
663
+ const published = this.publishAppEvent(event, payload, source, deliveryContext);
418
664
  if (isPromiseLike(published))
419
665
  await published;
420
666
  }
@@ -457,17 +703,27 @@ export class MessageDeliveryEngine {
457
703
  }
458
704
  return result;
459
705
  }
460
- normalizePublishedMessagePayload(event, payload) {
706
+ normalizePublishedMessagePayload(event, payload, source = 'direct') {
461
707
  payload = this.stripInlineInternalFields(payload);
708
+ let normalized;
462
709
  if (this.isInstanceScopedMessageEvent(event)) {
463
710
  if (event === 'group.message_created')
464
711
  payload = normalizeGroupMentionMode(payload);
465
- return this.attachAppMessageEnvelope(this.stripInternalSenderDeviceFields(this.attachCurrentInstanceContext(payload)));
712
+ normalized = this.attachAppMessageEnvelope(this.stripInternalSenderDeviceFields(this.attachCurrentInstanceContext(payload)));
466
713
  }
467
- if (this.isGroupScopedEvent(event)) {
468
- return this.attachAppGroupEventEnvelope(this.attachCurrentInstanceContext(payload));
714
+ else if (this.isGroupScopedEvent(event)) {
715
+ normalized = this.attachAppGroupEventEnvelope(this.attachCurrentInstanceContext(payload));
469
716
  }
470
- return payload;
717
+ else {
718
+ normalized = payload;
719
+ }
720
+ if (source === 'direct' || !isJsonObject(normalized))
721
+ return normalized;
722
+ const canonicalSource = source === 'group-push' || source === 'ordered' || source === 'legacy' || source === 'tail'
723
+ ? (source === 'tail' ? 'pull' : 'push')
724
+ : source === 'inline-push' || source === 'inline_push' ? 'inline_push'
725
+ : source === 'pending-retry' || source === 'pending_retry' ? 'pending_retry' : source;
726
+ return { ...normalized, source: canonicalSource };
471
727
  }
472
728
  stripInlineInternalFields(payload) {
473
729
  if (!isJsonObject(payload))
@@ -737,7 +993,7 @@ export class MessageDeliveryEngine {
737
993
  return `p2p|tombstone:${tombstoneId}`;
738
994
  return `p2p|unknown:${Date.now()}:${Math.random()}`;
739
995
  }
740
- async publishMessageRecallTombstone(seq, message) {
996
+ async publishMessageRecallTombstone(seq, message, source = 'push') {
741
997
  const client = this.runtime.client;
742
998
  const eventPayload = this.recallEventFromMessage(message);
743
999
  if (!eventPayload)
@@ -758,7 +1014,7 @@ export class MessageDeliveryEngine {
758
1014
  for (const [oldKey] of drop)
759
1015
  seen.delete(oldKey);
760
1016
  }
761
- await client._publishAppEvent('message.recalled', eventPayload, 'message-recall');
1017
+ await client._publishAppEvent('message.recalled', eventPayload, source);
762
1018
  client._clientLog.debug(`message.recalled published: seq=${String(seq)} ids=${JSON.stringify(eventPayload.message_ids)}`);
763
1019
  return true;
764
1020
  }
@@ -841,7 +1097,7 @@ export class MessageDeliveryEngine {
841
1097
  return `${normalizedGroupId}|tombstone:${tombstoneId}`;
842
1098
  return `${normalizedGroupId}|unknown:${Date.now()}:${Math.random()}`;
843
1099
  }
844
- async publishGroupRecallTombstone(groupId, seq, message) {
1100
+ async publishGroupRecallTombstone(groupId, seq, message, source = 'push') {
845
1101
  const client = this.runtime.client;
846
1102
  const eventPayload = this.recallEventFromGroupMessage(message);
847
1103
  if (!eventPayload)
@@ -864,7 +1120,7 @@ export class MessageDeliveryEngine {
864
1120
  for (const [oldKey] of drop)
865
1121
  seen.delete(oldKey);
866
1122
  }
867
- await client._publishAppEvent('group.message_recalled', eventPayload, 'group-recall');
1123
+ await client._publishAppEvent('group.message_recalled', eventPayload, source);
868
1124
  client._clientLog.debug(`group.message_recalled published: group=${groupId} seq=${String(seq)} ids=${JSON.stringify(eventPayload.message_ids)}`);
869
1125
  return true;
870
1126
  }
@@ -934,7 +1190,7 @@ export class MessageDeliveryEngine {
934
1190
  if (contig !== contigBefore)
935
1191
  this.persistSeq(ns);
936
1192
  }
937
- publishAppEvent(event, payload, source = 'direct') {
1193
+ publishAppEvent(event, payload, source = 'direct', deliveryContext) {
938
1194
  const client = this.runtime.client;
939
1195
  if ((event === 'message.received' || event === 'group.message_created') && isJsonObject(payload)) {
940
1196
  client._maybeAppendEchoTraceReceive(payload);
@@ -954,7 +1210,23 @@ export class MessageDeliveryEngine {
954
1210
  client._clientLog.debug(`agent_md etag inject skipped: ${err instanceof Error ? err.message : String(err)}`);
955
1211
  }
956
1212
  }
957
- return client._dispatcher.publishSyncAware(event, this.normalizePublishedMessagePayload(event, payload));
1213
+ const generation = this.deliveryChangedGeneration;
1214
+ const normalized = this.normalizePublishedMessagePayload(event, payload, source);
1215
+ const dispatcher = client._dispatcher;
1216
+ const result = typeof dispatcher.enqueue === 'function'
1217
+ ? (dispatcher.enqueue(event, normalized), undefined)
1218
+ : typeof dispatcher.publishSyncAware === 'function'
1219
+ ? dispatcher.publishSyncAware(event, normalized)
1220
+ : dispatcher.publish?.(event, normalized);
1221
+ const note = () => {
1222
+ if (generation === this.deliveryChangedGeneration) {
1223
+ this.noteDeliveryChanged(event, payload, source, deliveryContext);
1224
+ }
1225
+ };
1226
+ if (isPromiseLike(result))
1227
+ return Promise.resolve(result).then(note);
1228
+ note();
1229
+ return result;
958
1230
  }
959
1231
  messagePayloadForDebug(message) {
960
1232
  if (!isJsonObject(message))
@@ -1140,7 +1412,7 @@ export class MessageDeliveryEngine {
1140
1412
  _decrypt_error: String(exc),
1141
1413
  };
1142
1414
  client._attachV2EnvelopeMetadataFromSource(safeEvent, data);
1143
- Promise.resolve(client._publishAppEvent('message.undecryptable', safeEvent)).catch(() => { });
1415
+ Promise.resolve(client._publishAppEvent('message.undecryptable', safeEvent, 'push')).catch(() => { });
1144
1416
  }
1145
1417
  });
1146
1418
  client._clientLog.debug(`_onRawMessageReceived exit: elapsed=${Date.now() - tStart}ms (handler dispatched)`);
@@ -1272,7 +1544,7 @@ export class MessageDeliveryEngine {
1272
1544
  _decrypt_error: String(exc),
1273
1545
  };
1274
1546
  client._attachV2EnvelopeMetadataFromSource(safeEvent, data);
1275
- Promise.resolve(client._publishAppEvent('group.message_undecryptable', safeEvent)).catch(() => { });
1547
+ Promise.resolve(client._publishAppEvent('group.message_undecryptable', safeEvent, 'push')).catch(() => { });
1276
1548
  }
1277
1549
  });
1278
1550
  client._clientLog.debug(`_onRawGroupMessageCreated exit: elapsed=${Date.now() - tStart}ms (handler dispatched)`);
@@ -1368,7 +1640,7 @@ export class MessageDeliveryEngine {
1368
1640
  const client = this.runtime.client;
1369
1641
  let groupId = String(notification.group_id ?? '').trim();
1370
1642
  if (!groupId) {
1371
- await client._publishAppEvent('group.message_created', notification);
1643
+ await client._publishAppEvent('group.message_created', notification, 'push');
1372
1644
  return;
1373
1645
  }
1374
1646
  if (client._sessionOptions?.background_sync === false) {
@@ -1518,8 +1790,10 @@ export class MessageDeliveryEngine {
1518
1790
  }
1519
1791
  }
1520
1792
  client._gapFillDone.set(dedupKey, Date.now());
1793
+ this.syncStarted(ns);
1521
1794
  let filled = 0;
1522
1795
  let continuationAfterSeq = 0;
1796
+ let failed = false;
1523
1797
  try {
1524
1798
  let nextAfterSeq = afterSeq;
1525
1799
  const maxPages = singlePage ? 1 : 100;
@@ -1571,6 +1845,7 @@ export class MessageDeliveryEngine {
1571
1845
  }
1572
1846
  const eventSeqs = [];
1573
1847
  let hasDissolvedEvent = false;
1848
+ let publishedEventCount = 0;
1574
1849
  for (const evt of eventObjects) {
1575
1850
  const eventSeq = Number(evt.event_seq ?? 0);
1576
1851
  if (Number.isFinite(eventSeq) && eventSeq > 0)
@@ -1590,10 +1865,12 @@ export class MessageDeliveryEngine {
1590
1865
  }
1591
1866
  }
1592
1867
  if (Number.isFinite(eventSeq) && eventSeq > 0 && !client._pushedSeqs.get(ns)?.has(eventSeq)) {
1593
- this.enqueueOrderedMessage(ns, 'group.changed', eventSeq, evt);
1868
+ this.enqueueOrderedMessage(ns, 'group.changed', eventSeq, evt, 'pull');
1869
+ publishedEventCount += 1;
1594
1870
  }
1595
1871
  }
1596
- filled += 1;
1872
+ if (et !== 'group.message_created')
1873
+ filled += 1;
1597
1874
  }
1598
1875
  const ackContig = client._seqTracker.getContiguousSeq(ns);
1599
1876
  await this.drainOrderedMessages(ns);
@@ -1620,6 +1897,15 @@ export class MessageDeliveryEngine {
1620
1897
  client._clientLog.debug(`group event auto-ack failed: group=${groupId} ${formatDeliveryError(e)}`);
1621
1898
  }
1622
1899
  }
1900
+ this.syncProgress(ns, {
1901
+ rawCount: eventObjects.length,
1902
+ pulledCount: publishedEventCount,
1903
+ remaining: result.remaining,
1904
+ hasMore: typeof result.has_more === 'boolean'
1905
+ ? Boolean(result.has_more) : undefined,
1906
+ currentSeq: ackContig,
1907
+ targetSeq: Math.max(Number(cursor?.latest_seq ?? 0), Number(result.latest_event_seq ?? 0)),
1908
+ });
1623
1909
  const nextAfter = Math.max(eventSeqs.length > 0 ? Math.max(...eventSeqs) : nextAfterSeq, nextAfterSeq);
1624
1910
  if (singlePage && result.has_more === true && nextAfter > nextAfterSeq) {
1625
1911
  continuationAfterSeq = nextAfter;
@@ -1634,6 +1920,7 @@ export class MessageDeliveryEngine {
1634
1920
  client._clientLog.debug(`group event gap fill done: group=${groupId}, after_seq=${afterSeq}, filled=${filled}`);
1635
1921
  }
1636
1922
  catch (exc) {
1923
+ failed = true;
1637
1924
  client._clientLog.warn(`group event gap fill failed: ${formatDeliveryError(exc)}`);
1638
1925
  }
1639
1926
  finally {
@@ -1650,6 +1937,8 @@ export class MessageDeliveryEngine {
1650
1937
  else if (!singlePage && filled > 0 && client._seqTracker.getContiguousSeq(ns) > afterSeq) {
1651
1938
  void this.fillGroupEventGap(groupId);
1652
1939
  }
1940
+ if (!client._rpcPipeline)
1941
+ this.syncStopped(failed ? 'aborted' : 'pull_drained');
1653
1942
  }
1654
1943
  }
1655
1944
  enqueueOnlineUnreadEventHint(data) {
@@ -2845,6 +3134,7 @@ export class MessageDeliveryEngine {
2845
3134
  targets.delete(ns);
2846
3135
  if (tasks.get(ns) === task)
2847
3136
  tasks.delete(ns);
3137
+ client._rpcPipeline?.requestPullDrainedCheck?.();
2848
3138
  }
2849
3139
  });
2850
3140
  tasks.set(ns, task);
@@ -2975,13 +3265,13 @@ export class MessageDeliveryEngine {
2975
3265
  catch (exc) {
2976
3266
  const error = formatDeliveryError(exc);
2977
3267
  client._clientLog.warn(`restore SeqTracker state failed: ${error}`);
2978
- client._dispatcher.publish('seq_tracker.persist_error', {
3268
+ client._dispatcher.enqueue('seq_tracker.persist_error', {
2979
3269
  phase: 'restore',
2980
3270
  aid: client._aid,
2981
3271
  device_id: client._deviceId,
2982
3272
  slot_id: client._slotId,
2983
3273
  error: String(error),
2984
- }).catch(() => { });
3274
+ });
2985
3275
  }
2986
3276
  }
2987
3277
  migrateSeqStateGroupIds(state) {
@@ -3200,13 +3490,13 @@ export class MessageDeliveryEngine {
3200
3490
  catch (exc) {
3201
3491
  const error = formatDeliveryError(exc);
3202
3492
  client._clientLog.warn(`save SeqTracker state failed: ${error}`);
3203
- client._dispatcher.publish('seq_tracker.persist_error', {
3493
+ client._dispatcher.enqueue('seq_tracker.persist_error', {
3204
3494
  phase: 'save',
3205
3495
  aid,
3206
3496
  device_id: deviceId,
3207
3497
  slot_id: slotId,
3208
3498
  error: String(error),
3209
- }).catch(() => { });
3499
+ });
3210
3500
  if (throwOnError)
3211
3501
  throw exc;
3212
3502
  }
@@ -3324,45 +3614,67 @@ export class MessageDeliveryEngine {
3324
3614
  }
3325
3615
  return params;
3326
3616
  }
3327
- async drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true) {
3617
+ async drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true, source = 'push', context) {
3328
3618
  const client = this.runtime.client;
3329
3619
  this.ensurePullOperationCurrent();
3330
3620
  const queue = client._pendingOrderedMsgs.get(ns);
3331
3621
  if (!queue || queue.size === 0)
3332
3622
  return;
3333
3623
  let delivered = false;
3334
- while (true) {
3335
- this.ensurePullOperationCurrent();
3336
- const contig = client._seqTracker.getContiguousSeq(ns);
3337
- const ready = [...queue.keys()]
3338
- .filter((seq) => seq <= contig && (beforeSeq === undefined || seq < beforeSeq))
3339
- .sort((a, b) => a - b);
3340
- let seq = ready[0];
3341
- if (seq === undefined) {
3342
- const nextSeq = contig + 1;
3343
- if (beforeSeq !== undefined && nextSeq >= beforeSeq)
3344
- break;
3345
- if (!queue.has(nextSeq))
3346
- break;
3347
- seq = nextSeq;
3348
- }
3349
- if (seq === undefined)
3350
- continue;
3351
- const item = queue.get(seq);
3352
- queue.delete(seq);
3353
- if (!item)
3354
- continue;
3355
- if (client._pushedSeqs.get(ns)?.has(seq)) {
3356
- client._clientLog.debug(`publish ordered drain skipped duplicate: ns=${ns}, seq=${seq}, event=${item.event}`);
3624
+ const rootTrigger = this.deliveryChangedTrigger(source);
3625
+ const drainContexts = new Map();
3626
+ try {
3627
+ while (true) {
3628
+ this.ensurePullOperationCurrent();
3629
+ const contig = client._seqTracker.getContiguousSeq(ns);
3630
+ const ready = [...queue.keys()]
3631
+ .filter((seq) => seq <= contig && (beforeSeq === undefined || seq < beforeSeq))
3632
+ .sort((a, b) => a - b);
3633
+ let seq = ready[0];
3634
+ if (seq === undefined) {
3635
+ const nextSeq = contig + 1;
3636
+ if (beforeSeq !== undefined && nextSeq >= beforeSeq)
3637
+ break;
3638
+ if (!queue.has(nextSeq))
3639
+ break;
3640
+ seq = nextSeq;
3641
+ }
3642
+ if (seq === undefined)
3643
+ continue;
3644
+ const item = queue.get(seq);
3645
+ queue.delete(seq);
3646
+ if (!item)
3647
+ continue;
3648
+ if (client._pushedSeqs.get(ns)?.has(seq)) {
3649
+ client._clientLog.debug(`publish ordered drain skipped duplicate: ns=${ns}, seq=${seq}, event=${item.event}`);
3650
+ client._markOrderedSeqDelivered(ns, seq);
3651
+ continue;
3652
+ }
3653
+ const itemSource = item.source ?? source;
3654
+ const itemTrigger = this.deliveryChangedTrigger(itemSource);
3655
+ let itemContext = itemTrigger === rootTrigger ? context : undefined;
3656
+ if (itemTrigger === 'push' || itemTrigger === 'inline_push') {
3657
+ if (!itemContext) {
3658
+ itemContext = drainContexts.get(itemTrigger);
3659
+ if (!itemContext) {
3660
+ itemContext = { generation: this.deliveryChangedGeneration, changes: new Map() };
3661
+ drainContexts.set(itemTrigger, itemContext);
3662
+ }
3663
+ }
3664
+ }
3665
+ await this.publishOrderedQueueItem(ns, item.event, seq, item.payload, itemSource, pullResponse, itemContext);
3666
+ this.ensurePullOperationCurrent();
3667
+ this.markPublishedSeq(ns, seq);
3357
3668
  client._markOrderedSeqDelivered(ns, seq);
3358
- continue;
3669
+ delivered = true;
3670
+ client._clientLog.debug(`publish ordered drain delivered: ns=${ns}, seq=${seq}, event=${item.event}`);
3671
+ }
3672
+ }
3673
+ finally {
3674
+ for (const [trigger, drainContext] of drainContexts) {
3675
+ if (drainContext !== context)
3676
+ this.flushDeliveryChangedContext(drainContext, trigger);
3359
3677
  }
3360
- await this.publishOrderedQueueItem(ns, item.event, seq, item.payload, 'ordered-drain', pullResponse);
3361
- this.ensurePullOperationCurrent();
3362
- this.markPublishedSeq(ns, seq);
3363
- client._markOrderedSeqDelivered(ns, seq);
3364
- delivered = true;
3365
- client._clientLog.debug(`publish ordered drain delivered: ns=${ns}, seq=${seq}, event=${item.event}`);
3366
3678
  }
3367
3679
  if (queue.size === 0) {
3368
3680
  client._pendingOrderedMsgs.delete(ns);
@@ -3372,13 +3684,35 @@ export class MessageDeliveryEngine {
3372
3684
  }
3373
3685
  }
3374
3686
  }
3375
- async publishOrderedMessage(event, ns, seq, payload) {
3687
+ async publishOrderedMessage(event, ns, seq, payload, source = 'push') {
3688
+ const context = {
3689
+ generation: this.deliveryChangedGeneration,
3690
+ changes: new Map(),
3691
+ };
3692
+ try {
3693
+ return await this.publishOrderedMessageInternal(event, ns, seq, payload, source, context);
3694
+ }
3695
+ finally {
3696
+ const trigger = this.deliveryChangedTrigger(source);
3697
+ if (trigger === 'pull_drained' && context.generation === this.deliveryChangedGeneration) {
3698
+ const pending = this.pendingDeliveryChanges ?? new Map();
3699
+ this.pendingDeliveryChanges = pending;
3700
+ for (const [namespace, count] of context.changes) {
3701
+ pending.set(namespace, (pending.get(namespace) ?? 0) + count);
3702
+ }
3703
+ }
3704
+ else if (trigger && trigger !== 'pull_drained') {
3705
+ this.flushDeliveryChangedContext(context, trigger);
3706
+ }
3707
+ }
3708
+ }
3709
+ async publishOrderedMessageInternal(event, ns, seq, payload, source, context) {
3376
3710
  const client = this.runtime.client;
3377
3711
  this.ensurePullOperationCurrent();
3378
3712
  const seqNum = Number(seq);
3379
3713
  if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0) {
3380
3714
  client._clientLog.debug(`publish ordered direct(no-seq): event=${event}, ns=${ns || '<none>'}, seq=${String(seq)}`);
3381
- await this.publishOrderedQueueItem(ns, event, seqNum, payload, 'ordered');
3715
+ await this.publishOrderedQueueItem(ns, event, seqNum, payload, source, false, context);
3382
3716
  this.ensurePullOperationCurrent();
3383
3717
  return true;
3384
3718
  }
@@ -3393,10 +3727,10 @@ export class MessageDeliveryEngine {
3393
3727
  const contig = client._seqTracker.getContiguousSeq(ns);
3394
3728
  if (seqNum > contig && seqNum !== contig + 1) {
3395
3729
  client._clientLog.debug(`publish ordered enqueue(gap): event=${event}, ns=${ns}, seq=${seqNum}, contiguous=${contig}`);
3396
- this.enqueueOrderedMessage(ns, event, seqNum, payload);
3730
+ this.enqueueOrderedMessage(ns, event, seqNum, payload, source);
3397
3731
  return false;
3398
3732
  }
3399
- await this.drainOrderedMessages(ns, seqNum);
3733
+ await this.drainOrderedMessages(ns, seqNum, false, true, source, context);
3400
3734
  this.ensurePullOperationCurrent();
3401
3735
  if (client._pushedSeqs.get(ns)?.has(seqNum)) {
3402
3736
  client._clientLog.debug(`publish ordered skipped after-drain duplicate: event=${event}, ns=${ns}, seq=${seqNum}`);
@@ -3406,12 +3740,12 @@ export class MessageDeliveryEngine {
3406
3740
  queue?.delete(seqNum);
3407
3741
  if (queue && queue.size === 0)
3408
3742
  client._pendingOrderedMsgs.delete(ns);
3409
- await this.publishOrderedQueueItem(ns, event, seqNum, payload, 'ordered');
3743
+ await this.publishOrderedQueueItem(ns, event, seqNum, payload, source, false, context);
3410
3744
  this.ensurePullOperationCurrent();
3411
3745
  this.markPublishedSeq(ns, seqNum);
3412
3746
  client._markOrderedSeqDelivered(ns, seqNum);
3413
3747
  client._clientLog.debug(`publish ordered delivered: event=${event}, ns=${ns}, seq=${seqNum}`);
3414
- await this.drainOrderedMessages(ns);
3748
+ await this.drainOrderedMessages(ns, undefined, false, true, source, context);
3415
3749
  if (!client._pendingOrderedMsgs.get(ns)) {
3416
3750
  this.ensurePullOperationCurrent();
3417
3751
  await this.saveSeqTrackerState();
@@ -3444,18 +3778,18 @@ export class MessageDeliveryEngine {
3444
3778
  }
3445
3779
  return true;
3446
3780
  }
3447
- async publishPulledMessage(event, ns, seq, payload, persist = true) {
3781
+ async publishPulledMessage(event, ns, seq, payload, persist = true, source = 'pull') {
3448
3782
  const client = this.runtime.client;
3449
3783
  this.ensurePullOperationCurrent();
3450
3784
  const seqNum = Number(seq);
3451
3785
  if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0 || !ns) {
3452
3786
  client._clientLog.debug(`publish pulled direct(no-seq): event=${event}, ns=${ns || '<none>'}, seq=${String(seq)}`);
3453
3787
  if (event === 'message.recalled') {
3454
- const published = await this.publishMessageRecallTombstone(seq, payload);
3788
+ const published = await this.publishMessageRecallTombstone(seq, payload, source);
3455
3789
  this.ensurePullOperationCurrent();
3456
3790
  return published;
3457
3791
  }
3458
- const published = client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, 'pull'));
3792
+ const published = client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, source));
3459
3793
  if (isPromiseLike(published))
3460
3794
  await published;
3461
3795
  this.ensurePullOperationCurrent();
@@ -3469,27 +3803,27 @@ export class MessageDeliveryEngine {
3469
3803
  client._pendingOrderedMsgs.delete(ns);
3470
3804
  return false;
3471
3805
  }
3472
- await this.drainOrderedMessages(ns, seqNum, true, persist);
3806
+ await this.drainOrderedMessages(ns, seqNum, true, persist, source);
3473
3807
  this.ensurePullOperationCurrent();
3474
3808
  queue?.delete(seqNum);
3475
3809
  if (queue && queue.size === 0)
3476
3810
  client._pendingOrderedMsgs.delete(ns);
3477
3811
  if (event === 'message.recalled') {
3478
- const recallPublished = await this.publishMessageRecallTombstone(seqNum, payload);
3812
+ const recallPublished = await this.publishMessageRecallTombstone(seqNum, payload, source);
3479
3813
  this.ensurePullOperationCurrent();
3480
3814
  this.markPublishedSeq(ns, seqNum);
3481
3815
  client._clientLog.debug(`publish pulled delivered: event=${event}, ns=${ns}, seq=${seqNum}`);
3482
- await this.drainOrderedMessages(ns, undefined, true, persist);
3816
+ await this.drainOrderedMessages(ns, undefined, true, persist, source);
3483
3817
  this.ensurePullOperationCurrent();
3484
3818
  return recallPublished;
3485
3819
  }
3486
- const published = client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, 'pull'));
3820
+ const published = client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, source));
3487
3821
  if (isPromiseLike(published))
3488
3822
  await published;
3489
3823
  this.ensurePullOperationCurrent();
3490
3824
  this.markPublishedSeq(ns, seqNum);
3491
3825
  client._clientLog.debug(`publish pulled delivered: event=${event}, ns=${ns}, seq=${seqNum}`);
3492
- await this.drainOrderedMessages(ns, undefined, true, persist);
3826
+ await this.drainOrderedMessages(ns, undefined, true, persist, source);
3493
3827
  this.ensurePullOperationCurrent();
3494
3828
  return true;
3495
3829
  }