@agentunion/fastaun 0.5.10 → 0.5.12

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.
@@ -50,6 +50,10 @@ function positiveSafeSequenceHint(value) {
50
50
  const parsed = Number(value);
51
51
  return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 0;
52
52
  }
53
+ function hasNonEmptyInlineMessage(value) {
54
+ return isJsonObject(value)
55
+ && Object.keys(value).length > 0;
56
+ }
53
57
  function nonNegativeSafeSequenceHint(value) {
54
58
  return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
55
59
  }
@@ -80,6 +84,8 @@ export class MessageDeliveryEngine {
80
84
  inlineAckGeneration = 0;
81
85
  pendingDeliveryChanges = null;
82
86
  deliveryChangedGeneration = 0;
87
+ syncRun = 0;
88
+ syncSessions = null;
83
89
  // 同一实例的 tracker 写入必须串行,避免旧的延迟 flush 覆盖新的 Forward 提交。
84
90
  seqTrackerWriteChain = Promise.resolve();
85
91
  constructor(runtime) {
@@ -152,6 +158,7 @@ export class MessageDeliveryEngine {
152
158
  }
153
159
  const client = this.runtime.client;
154
160
  this.ensurePullOperationCurrent();
161
+ this.syncStarted(ns);
155
162
  const response = result;
156
163
  const messages = deduplicatePlainForwardPageMessages(response.messages, afterSeq);
157
164
  const coordinator = this.forwardCoordinator();
@@ -194,20 +201,24 @@ export class MessageDeliveryEngine {
194
201
  }
195
202
  const message = normalizeGroupMentionMode(rawMessage);
196
203
  if (this.recallEventFromGroupMessage(message)) {
197
- if (await this.publishGroupRecallTombstone(groupId, seq, message)) {
204
+ if (await this.publishGroupRecallTombstone(groupId, seq, message, 'pull')) {
198
205
  this.ensurePullOperationCurrent();
199
206
  this.markPublishedSeq(ns, seq);
200
207
  publishedCount += 1;
201
208
  }
202
209
  }
210
+ else if (this.isSelfSentGroupMessage(message)) {
211
+ this.markPublishedSeq(ns, seq);
212
+ }
203
213
  else if (await this.publishPulledMessage('group.message_created', ns, seq, message, false)) {
204
214
  this.ensurePullOperationCurrent();
205
215
  publishedCount += 1;
206
216
  }
207
217
  }
208
218
  this.ensurePullOperationCurrent();
209
- if (messages.length > 0)
210
- client._seqTracker.onPullResult(ns, messages, afterSeq);
219
+ client._seqTracker.onPullResult(ns, messages, afterSeq, Number.isSafeInteger(result.contiguous_seq)
220
+ ? result.contiguous_seq
221
+ : undefined);
211
222
  const commitTarget = Math.max(client._seqTracker.getContiguousSeq(ns), retentionFloor, visibilityFloor, deferredServerCursor);
212
223
  if (commitTarget > client._seqTracker.getContiguousSeq(ns)) {
213
224
  client._seqTracker.forceContiguousSeq(ns, commitTarget);
@@ -261,9 +272,17 @@ export class MessageDeliveryEngine {
261
272
  this.ensurePullOperationCurrent();
262
273
  await this.confirmPlainForwardAck(ns, ackMethod, pendingAckSeq, groupId);
263
274
  }
275
+ this.syncProgress(ns, {
276
+ rawCount: messages.length,
277
+ pulledCount: publishedCount,
278
+ remaining: response.remaining,
279
+ hasMore: typeof response.has_more === 'boolean' ? response.has_more : undefined,
280
+ currentSeq: client._seqTracker.getContiguousSeq(ns),
281
+ });
264
282
  return { rawCount: messages.length, publishedCount };
265
283
  }
266
284
  resetInlineAckState() {
285
+ this.syncStopped('aborted');
267
286
  this.deliveryChangedGeneration += 1;
268
287
  this.pendingDeliveryChanges = null;
269
288
  this.inlineAckGeneration += 1;
@@ -276,6 +295,7 @@ export class MessageDeliveryEngine {
276
295
  this.onlineUnreadHintTargets = null;
277
296
  this.onlineUnreadHintTasks = null;
278
297
  this.realtimeTailHeads = null;
298
+ this.syncSessions = null;
279
299
  }
280
300
  deliveryChangedNamespace(event, payload) {
281
301
  if (event === 'group.message_created' && isJsonObject(payload)) {
@@ -301,9 +321,9 @@ export class MessageDeliveryEngine {
301
321
  || (typeof seq === 'number' && Number.isSafeInteger(seq) && seq > 0);
302
322
  }
303
323
  deliveryChangedTrigger(source) {
304
- if (source === 'inline-push')
324
+ if (source === 'inline-push' || source === 'inline_push')
305
325
  return 'inline_push';
306
- if (source === 'pull' || source === 'tail')
326
+ if (source === 'pull' || source === 'tail' || source === 'pending-retry' || source === 'pending_retry')
307
327
  return 'pull_drained';
308
328
  if (source === 'push' || source === 'group-push' || source === 'ordered')
309
329
  return 'push';
@@ -336,7 +356,7 @@ export class MessageDeliveryEngine {
336
356
  pending.clear();
337
357
  if (changes.length === 0)
338
358
  return;
339
- const payload = { trigger, changes };
359
+ const payload = { source: trigger === 'pull_drained' ? 'pull' : trigger, trigger, changes };
340
360
  const dispatcher = this.runtime.client._dispatcher;
341
361
  const published = typeof dispatcher.enqueue === 'function'
342
362
  ? (dispatcher.enqueue('delivery.changed', payload), undefined)
@@ -348,8 +368,119 @@ export class MessageDeliveryEngine {
348
368
  this.runtime.client._clientLog.warn(`delivery.changed handler failed: ${formatDeliveryError(exc)}`);
349
369
  });
350
370
  }
371
+ publishSyncLifecycle(event, payload) {
372
+ const dispatcher = this.runtime.client._dispatcher;
373
+ const published = typeof dispatcher.enqueue === 'function'
374
+ ? (dispatcher.enqueue(event, payload), undefined)
375
+ : typeof dispatcher.publishSyncAware === 'function'
376
+ ? dispatcher.publishSyncAware(event, payload)
377
+ : dispatcher.publish?.(event, payload);
378
+ if (isPromiseLike(published))
379
+ void Promise.resolve(published).catch((exc) => {
380
+ this.runtime.client._clientLog.warn(`${event} handler failed: ${formatDeliveryError(exc)}`);
381
+ });
382
+ }
383
+ syncStarted(ns) {
384
+ const namespace = String(ns ?? '').trim();
385
+ if (!namespace)
386
+ return;
387
+ const sessions = this.syncSessions ?? new Map();
388
+ this.syncSessions = sessions;
389
+ if (sessions.has(namespace))
390
+ return;
391
+ if (sessions.size === 0) {
392
+ this.syncRun += 1;
393
+ this.publishSyncLifecycle('sync.started', {
394
+ run_id: this.syncRun,
395
+ source: 'pull',
396
+ syncing: true,
397
+ namespaces_pending: 1,
398
+ received_total: 0,
399
+ namespace,
400
+ started_at: Date.now(),
401
+ });
402
+ }
403
+ const session = { pages: 0, pulled: 0, startedAt: Date.now() };
404
+ sessions.set(namespace, session);
405
+ }
406
+ syncProgress(ns, progress = {}) {
407
+ const namespace = String(ns ?? '').trim();
408
+ if (!namespace)
409
+ return;
410
+ this.syncStarted(namespace);
411
+ const session = this.syncSessions?.get(namespace);
412
+ if (!session)
413
+ return;
414
+ const pulledCount = Math.max(0, Number(progress.pulledCount ?? progress.rawCount ?? 0) || 0);
415
+ const rawCount = Math.max(0, Number(progress.rawCount ?? pulledCount) || 0);
416
+ session.pages += 1;
417
+ session.pulled += pulledCount;
418
+ const payload = {
419
+ run_id: this.syncRun,
420
+ source: 'pull',
421
+ namespace,
422
+ page: session.pages,
423
+ page_pulled_count: pulledCount,
424
+ page_raw_count: rawCount,
425
+ pulled_count: session.pulled,
426
+ received_total: session.pulled,
427
+ batch_size: pulledCount,
428
+ };
429
+ if (typeof progress.remaining === 'number'
430
+ && Number.isSafeInteger(progress.remaining) && progress.remaining >= 0) {
431
+ payload.remaining = progress.remaining;
432
+ payload.estimated_remaining = payload.remaining;
433
+ }
434
+ if (progress.hasMore !== undefined) {
435
+ payload.has_more = Boolean(progress.hasMore);
436
+ if (progress.remaining === undefined || progress.remaining === null) {
437
+ payload.estimated_remaining = progress.hasMore ? undefined : 0;
438
+ }
439
+ }
440
+ if (progress.currentSeq !== undefined && Number.isFinite(Number(progress.currentSeq))) {
441
+ payload.current_seq = Math.max(0, Number(progress.currentSeq));
442
+ }
443
+ if (progress.targetSeq !== undefined && Number.isFinite(Number(progress.targetSeq))) {
444
+ payload.target_seq = Math.max(0, Number(progress.targetSeq));
445
+ }
446
+ this.publishSyncLifecycle('sync.progress', payload);
447
+ }
351
448
  onPullDrained() {
352
449
  this.flushDeliveryChanged('pull_drained');
450
+ this.syncStopped('pull_drained');
451
+ }
452
+ onPullAborted() {
453
+ this.syncStopped('aborted');
454
+ }
455
+ syncStopped(reason = 'completed') {
456
+ const sessions = this.syncSessions;
457
+ if (!sessions || sessions.size === 0)
458
+ return;
459
+ const entries = [...sessions.entries()];
460
+ const receivedTotal = entries.reduce((total, [, session]) => total + session.pulled, 0);
461
+ const payload = {
462
+ run_id: this.syncRun,
463
+ source: 'pull',
464
+ syncing: false,
465
+ namespaces_pending: 0,
466
+ received_total: receivedTotal,
467
+ reason,
468
+ stopped_at: Date.now(),
469
+ };
470
+ if (entries.length === 1) {
471
+ const [namespace, session] = entries[0];
472
+ payload.namespace = namespace;
473
+ payload.pages = session.pages;
474
+ payload.pulled_count = session.pulled;
475
+ }
476
+ else {
477
+ payload.namespaces = entries.map(([namespace]) => namespace);
478
+ payload.pages = entries.reduce((total, [, session]) => total + session.pages, 0);
479
+ payload.pulled_count = receivedTotal;
480
+ }
481
+ sessions.clear();
482
+ this.syncSessions = null;
483
+ this.publishSyncLifecycle('sync.stopped', payload);
353
484
  }
354
485
  flushDeliveryChangedContext(context, trigger) {
355
486
  if (context.generation !== this.deliveryChangedGeneration || context.changes.size === 0)
@@ -369,7 +500,7 @@ export class MessageDeliveryEngine {
369
500
  if (changes.length === 0)
370
501
  return;
371
502
  const dispatcher = this.runtime.client._dispatcher;
372
- const payload = { trigger, changes };
503
+ const payload = { trigger, source: trigger, changes };
373
504
  const published = typeof dispatcher.enqueue === 'function'
374
505
  ? (dispatcher.enqueue('delivery.changed', payload), undefined)
375
506
  : typeof dispatcher.publishSyncAware === 'function'
@@ -431,6 +562,9 @@ export class MessageDeliveryEngine {
431
562
  hasPendingPull(ns) {
432
563
  return (this.pendingGroupPullUpper?.get(ns) ?? 0) > 0;
433
564
  }
565
+ pendingPullUpper(ns) {
566
+ return this.pendingGroupPullUpper?.get(ns) ?? 0;
567
+ }
434
568
  hasAnyPendingPull() {
435
569
  return [...(this.pendingGroupPullUpper?.values() ?? [])].some((seq) => seq > 0)
436
570
  || Boolean(this.onlineUnreadHintTasks?.size)
@@ -527,7 +661,7 @@ export class MessageDeliveryEngine {
527
661
  return;
528
662
  }
529
663
  if (event === 'message.recalled') {
530
- await this.publishMessageRecallTombstone(seq, payload);
664
+ await this.publishMessageRecallTombstone(seq, payload, source);
531
665
  this.ensurePullOperationCurrent();
532
666
  return;
533
667
  }
@@ -580,17 +714,27 @@ export class MessageDeliveryEngine {
580
714
  }
581
715
  return result;
582
716
  }
583
- normalizePublishedMessagePayload(event, payload) {
717
+ normalizePublishedMessagePayload(event, payload, source = 'direct') {
584
718
  payload = this.stripInlineInternalFields(payload);
719
+ let normalized;
585
720
  if (this.isInstanceScopedMessageEvent(event)) {
586
721
  if (event === 'group.message_created')
587
722
  payload = normalizeGroupMentionMode(payload);
588
- return this.attachAppMessageEnvelope(this.stripInternalSenderDeviceFields(this.attachCurrentInstanceContext(payload)));
723
+ normalized = this.attachAppMessageEnvelope(this.stripInternalSenderDeviceFields(this.attachCurrentInstanceContext(payload)));
724
+ }
725
+ else if (this.isGroupScopedEvent(event)) {
726
+ normalized = this.attachAppGroupEventEnvelope(this.attachCurrentInstanceContext(payload));
589
727
  }
590
- if (this.isGroupScopedEvent(event)) {
591
- return this.attachAppGroupEventEnvelope(this.attachCurrentInstanceContext(payload));
728
+ else {
729
+ normalized = payload;
592
730
  }
593
- return payload;
731
+ if (source === 'direct' || !isJsonObject(normalized))
732
+ return normalized;
733
+ const canonicalSource = source === 'group-push' || source === 'ordered' || source === 'legacy' || source === 'tail'
734
+ ? (source === 'tail' ? 'pull' : 'push')
735
+ : source === 'inline-push' || source === 'inline_push' ? 'inline_push'
736
+ : source === 'pending-retry' || source === 'pending_retry' ? 'pending_retry' : source;
737
+ return { ...normalized, source: canonicalSource };
594
738
  }
595
739
  stripInlineInternalFields(payload) {
596
740
  if (!isJsonObject(payload))
@@ -860,7 +1004,7 @@ export class MessageDeliveryEngine {
860
1004
  return `p2p|tombstone:${tombstoneId}`;
861
1005
  return `p2p|unknown:${Date.now()}:${Math.random()}`;
862
1006
  }
863
- async publishMessageRecallTombstone(seq, message) {
1007
+ async publishMessageRecallTombstone(seq, message, source = 'push') {
864
1008
  const client = this.runtime.client;
865
1009
  const eventPayload = this.recallEventFromMessage(message);
866
1010
  if (!eventPayload)
@@ -881,7 +1025,7 @@ export class MessageDeliveryEngine {
881
1025
  for (const [oldKey] of drop)
882
1026
  seen.delete(oldKey);
883
1027
  }
884
- await client._publishAppEvent('message.recalled', eventPayload, 'message-recall');
1028
+ await client._publishAppEvent('message.recalled', eventPayload, source);
885
1029
  client._clientLog.debug(`message.recalled published: seq=${String(seq)} ids=${JSON.stringify(eventPayload.message_ids)}`);
886
1030
  return true;
887
1031
  }
@@ -964,7 +1108,7 @@ export class MessageDeliveryEngine {
964
1108
  return `${normalizedGroupId}|tombstone:${tombstoneId}`;
965
1109
  return `${normalizedGroupId}|unknown:${Date.now()}:${Math.random()}`;
966
1110
  }
967
- async publishGroupRecallTombstone(groupId, seq, message) {
1111
+ async publishGroupRecallTombstone(groupId, seq, message, source = 'push') {
968
1112
  const client = this.runtime.client;
969
1113
  const eventPayload = this.recallEventFromGroupMessage(message);
970
1114
  if (!eventPayload)
@@ -987,7 +1131,7 @@ export class MessageDeliveryEngine {
987
1131
  for (const [oldKey] of drop)
988
1132
  seen.delete(oldKey);
989
1133
  }
990
- await client._publishAppEvent('group.message_recalled', eventPayload, 'group-recall');
1134
+ await client._publishAppEvent('group.message_recalled', eventPayload, source);
991
1135
  client._clientLog.debug(`group.message_recalled published: group=${groupId} seq=${String(seq)} ids=${JSON.stringify(eventPayload.message_ids)}`);
992
1136
  return true;
993
1137
  }
@@ -1027,35 +1171,11 @@ export class MessageDeliveryEngine {
1027
1171
  return;
1028
1172
  }
1029
1173
  const ns = `group:${groupId}`;
1030
- if (seqNum > 0) {
1174
+ if (seqNum > 0)
1031
1175
  client._seqTracker.updateMaxSeen(ns, seqNum);
1032
- if (client._seqTracker.getContiguousSeq(ns) === seqNum) {
1033
- // 已覆盖(pull 先到并推进过),仍走去重发布兜底,不重复推进 seq。
1034
- await this.publishGroupRecallTombstone(groupId, seq, wrapped);
1035
- return;
1036
- }
1037
- client._repairPushContiguousBound(ns, seqNum, true, '_raw.group.message_recalled');
1038
- }
1039
- // 该 notice_seq 已由 pull 路径处理过(已发布或挂起待发布)时,去重发布兜底后返回。
1040
- const pushed = client._pushedSeqs.get(ns);
1041
- const pending = client._pendingOrderedMsgs.get(ns);
1042
- if (pushed?.has(seqNum) || pending?.has(seqNum)) {
1043
- await this.publishGroupRecallTombstone(groupId, seq, wrapped);
1044
- return;
1176
+ if (seqNum > client._seqTracker.getContiguousSeq(ns) && client._sessionOptions?.background_sync !== false) {
1177
+ this.fillGroupGap(groupId).catch((exc) => client._clientLog.warn(`background recall gap fill failed: ${formatDeliveryError(exc)}`));
1045
1178
  }
1046
- const contigBefore = client._seqTracker.getContiguousSeq(ns);
1047
- client._seqTracker.onMessageSeq(ns, seqNum);
1048
- await this.publishGroupRecallTombstone(groupId, seq, wrapped);
1049
- this.markPublishedSeq(ns, seqNum);
1050
- client._markOrderedSeqDelivered?.(ns, seqNum);
1051
- const contig = client._seqTracker.getContiguousSeq(ns);
1052
- if (contig > 0) {
1053
- const ackSeq = this.clampAckSeq('group.ack_messages', 'msg_seq', ns, contig);
1054
- client._withBackgroundRpc(() => client._ackGroupV2(groupId, ackSeq))
1055
- .catch((e) => { client._clientLog.debug(`group recall auto-ack failed: group=${groupId} ${formatDeliveryError(e)}`); });
1056
- }
1057
- if (contig !== contigBefore)
1058
- this.persistSeq(ns);
1059
1179
  }
1060
1180
  publishAppEvent(event, payload, source = 'direct', deliveryContext) {
1061
1181
  const client = this.runtime.client;
@@ -1078,7 +1198,7 @@ export class MessageDeliveryEngine {
1078
1198
  }
1079
1199
  }
1080
1200
  const generation = this.deliveryChangedGeneration;
1081
- const normalized = this.normalizePublishedMessagePayload(event, payload);
1201
+ const normalized = this.normalizePublishedMessagePayload(event, payload, source);
1082
1202
  const dispatcher = client._dispatcher;
1083
1203
  const result = typeof dispatcher.enqueue === 'function'
1084
1204
  ? (dispatcher.enqueue(event, normalized), undefined)
@@ -1197,6 +1317,21 @@ export class MessageDeliveryEngine {
1197
1317
  }
1198
1318
  return true;
1199
1319
  }
1320
+ isSelfSentGroupMessage(message) {
1321
+ if (!isJsonObject(message))
1322
+ return false;
1323
+ const client = this.runtime.client;
1324
+ const selfAid = String(client._aid ?? '').trim().toLowerCase();
1325
+ const senderAid = String(message.sender_aid ?? message.from_aid ?? message.from ?? '').trim().toLowerCase();
1326
+ if (!selfAid || senderAid !== selfAid)
1327
+ return false;
1328
+ const senderDevice = Object.prototype.hasOwnProperty.call(message, 'sender_device_id')
1329
+ ? message.sender_device_id
1330
+ : message.from_device_id;
1331
+ if (typeof senderDevice !== 'string' || !senderDevice.trim() || !String(client._deviceId ?? '').trim())
1332
+ return false;
1333
+ return senderDevice.trim() === String(client._deviceId).trim();
1334
+ }
1200
1335
  async runPushProcessSerialized(ns, operation, inlineGeneration) {
1201
1336
  const client = this.runtime.client;
1202
1337
  if (inlineGeneration === undefined) {
@@ -1264,7 +1399,7 @@ export class MessageDeliveryEngine {
1264
1399
  ? `p2p:${client._aid}`
1265
1400
  : '';
1266
1401
  const inlineGeneration = isJsonObject(data)
1267
- && Object.prototype.hasOwnProperty.call(data, 'inline_message')
1402
+ && hasNonEmptyInlineMessage(data.inline_message)
1268
1403
  ? this.captureInlineAckGeneration() : undefined;
1269
1404
  this.runPushProcessSerialized(ns, () => this.processAndPublishMessage(data, inlineGeneration), inlineGeneration).catch((exc) => {
1270
1405
  client._clientLog.warn(`P2P message decrypt failed: ${formatDeliveryError(exc)}`);
@@ -1279,7 +1414,7 @@ export class MessageDeliveryEngine {
1279
1414
  _decrypt_error: String(exc),
1280
1415
  };
1281
1416
  client._attachV2EnvelopeMetadataFromSource(safeEvent, data);
1282
- Promise.resolve(client._publishAppEvent('message.undecryptable', safeEvent)).catch(() => { });
1417
+ Promise.resolve(client._publishAppEvent('message.undecryptable', safeEvent, 'push')).catch(() => { });
1283
1418
  }
1284
1419
  });
1285
1420
  client._clientLog.debug(`_onRawMessageReceived exit: elapsed=${Date.now() - tStart}ms (handler dispatched)`);
@@ -1299,20 +1434,9 @@ export class MessageDeliveryEngine {
1299
1434
  const seqNum = Number(seq);
1300
1435
  if (Number.isFinite(seqNum) && seqNum > 0)
1301
1436
  client._seqTracker.updateMaxSeen(ns, seqNum);
1302
- const contigBefore = client._seqTracker.getContiguousSeq(ns);
1303
- const published = await this.publishOrderedMessage('message.recalled', ns, seq, msg);
1304
- const contigAfter = client._seqTracker.getContiguousSeq(ns);
1305
- if (Number(seq) > contigAfter && !published && client._sessionOptions?.background_sync !== false) {
1437
+ if (Number(seq) > client._seqTracker.getContiguousSeq(ns) && client._sessionOptions?.background_sync !== false) {
1306
1438
  this.fillP2pGap().catch((exc) => client._clientLog.warn(`background recall gap fill failed: ${formatDeliveryError(exc)}`));
1307
1439
  }
1308
- const contig = client._seqTracker.getContiguousSeq(ns);
1309
- if (contig > 0) {
1310
- const ackSeq = this.clampAckSeq('message.ack', 'seq', ns, contig);
1311
- client._withBackgroundRpc(() => client._ackV2(ackSeq))
1312
- .catch((e) => { client._clientLog.debug(`P2P recall auto-ack failed: ${formatDeliveryError(e)}`); });
1313
- }
1314
- if (contigAfter !== contigBefore)
1315
- this.persistSeq(ns);
1316
1440
  return;
1317
1441
  }
1318
1442
  await this.publishMessageRecallTombstone(seq, msg);
@@ -1331,7 +1455,7 @@ export class MessageDeliveryEngine {
1331
1455
  // 新 SDK 的所有带合法 seq P2P Push 都只作为 Head 通知处理。
1332
1456
  // 新服务端为兼容旧 SDK 仍可能发送 message.received;V2 会话下必须
1333
1457
  // 在旧 seq tracking / ordered publish 之前转入前台 Tail 协调器。
1334
- const inlinePresent = Object.prototype.hasOwnProperty.call(msg, 'inline_message');
1458
+ const inlinePresent = hasNonEmptyInlineMessage(msg.inline_message);
1335
1459
  const legacyPushSeq = this.realtimeHeadSeq(msg.seq, inlinePresent);
1336
1460
  if (client._v2Session && client._aid && (msg.seq !== undefined && msg.seq !== null || inlinePresent)) {
1337
1461
  if (legacyPushSeq === null) {
@@ -1350,29 +1474,11 @@ export class MessageDeliveryEngine {
1350
1474
  const ns = `p2p:${client._aid}`;
1351
1475
  if (seq > 0)
1352
1476
  client._seqTracker.updateMaxSeen(ns, seq);
1353
- const contigBefore = client._seqTracker.getContiguousSeq(ns);
1354
- const published = encryptedPush
1355
- ? await client._publishEncryptedPushMessage('message.received', 'message.undecryptable', ns, seq, msg, false)
1356
- : await this.publishOrderedMessage('message.received', ns, seq, msg);
1357
- const contigAfter = client._seqTracker.getContiguousSeq(ns);
1358
- const needPull = Number(seq) > contigAfter && !published;
1359
- if (needPull && client._sessionOptions?.background_sync !== false) {
1360
- client._clientLog.debug(`P2P seq gap detected: ns=${ns}, seq=${seq}, contiguous=${contigAfter}`);
1477
+ if (Number(seq) > client._seqTracker.getContiguousSeq(ns) && client._sessionOptions?.background_sync !== false) {
1478
+ client._clientLog.debug(`P2P push triggering Forward: ns=${ns}, seq=${seq}, contiguous=${client._seqTracker.getContiguousSeq(ns)}`);
1361
1479
  this.fillP2pGap().catch((exc) => client._clientLog.warn(`background gap fill trigger failed: ${formatDeliveryError(exc)}`));
1362
1480
  }
1363
- const contig = client._seqTracker.getContiguousSeq(ns);
1364
- if (contig > 0) {
1365
- const maxSeen = client._seqTracker.getMaxSeenSeq(ns);
1366
- const ackSeq = this.clampAckSeq('message.ack', 'seq', ns, contig);
1367
- client._clientLog.debug(`P2P push auto-ack send: ns=${ns}, seq=${ackSeq}, contiguous=${contig}, max_seen=${maxSeen}`);
1368
- client._withBackgroundRpc(() => client._ackV2(ackSeq))
1369
- .then(() => { client._clientLog.debug(`P2P push auto-ack ok: ns=${ns}, seq=${ackSeq}`); })
1370
- .catch((e) => { client._clientLog.debug(`P2P auto-ack failed: ${formatDeliveryError(e)}`); });
1371
- }
1372
- if (contigAfter !== contigBefore)
1373
- this.persistSeq(ns);
1374
- if (encryptedPush)
1375
- return;
1481
+ return;
1376
1482
  }
1377
1483
  else {
1378
1484
  if (encryptedPush) {
@@ -1396,7 +1502,7 @@ export class MessageDeliveryEngine {
1396
1502
  ? String(data.group_id ?? '').trim()
1397
1503
  : '';
1398
1504
  const inlineGeneration = isJsonObject(data)
1399
- && Object.prototype.hasOwnProperty.call(data, 'inline_message')
1505
+ && hasNonEmptyInlineMessage(data.inline_message)
1400
1506
  ? this.captureInlineAckGeneration() : undefined;
1401
1507
  this.runPushProcessSerialized(groupNs ? `group:${groupNs}` : '', () => this.processAndPublishGroupMessage(data, inlineGeneration), inlineGeneration).catch((exc) => {
1402
1508
  client._clientLog.warn(`group message decrypt failed: ${formatDeliveryError(exc)}`);
@@ -1411,7 +1517,7 @@ export class MessageDeliveryEngine {
1411
1517
  _decrypt_error: String(exc),
1412
1518
  };
1413
1519
  client._attachV2EnvelopeMetadataFromSource(safeEvent, data);
1414
- Promise.resolve(client._publishAppEvent('group.message_undecryptable', safeEvent)).catch(() => { });
1520
+ Promise.resolve(client._publishAppEvent('group.message_undecryptable', safeEvent, 'push')).catch(() => { });
1415
1521
  }
1416
1522
  });
1417
1523
  client._clientLog.debug(`_onRawGroupMessageCreated exit: elapsed=${Date.now() - tStart}ms (handler dispatched)`);
@@ -1431,7 +1537,7 @@ export class MessageDeliveryEngine {
1431
1537
  }
1432
1538
  // V2 会话中旧 group.message_created 也只是 Head 通知;必须先 Tail,
1433
1539
  // 不能让 payload 路径直接推进旧 ordered publish 状态。
1434
- const inlinePresent = Object.prototype.hasOwnProperty.call(msg, 'inline_message');
1540
+ const inlinePresent = hasNonEmptyInlineMessage(msg.inline_message);
1435
1541
  const legacyPushSeq = this.realtimeHeadSeq(seq, inlinePresent);
1436
1542
  if (client._v2Session && groupId && (seq !== undefined && seq !== null || inlinePresent)) {
1437
1543
  if (legacyPushSeq === null) {
@@ -1456,46 +1562,15 @@ export class MessageDeliveryEngine {
1456
1562
  const ns = `group:${groupId}`;
1457
1563
  if (seq > 0)
1458
1564
  client._seqTracker.updateMaxSeen(ns, seq);
1459
- const contigBefore = client._seqTracker.getContiguousSeq(ns);
1460
- // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled,仍占 seq 推进 contiguous/ack。
1461
- if (!encryptedPush && this.recallEventFromGroupMessage(msg)) {
1462
- const published = await this.publishOrderedGroupRecall(ns, seq, msg);
1463
- const contigAfter = client._seqTracker.getContiguousSeq(ns);
1464
- const contig = client._seqTracker.getContiguousSeq(ns);
1465
- if (contig > 0) {
1466
- const ackSeq = this.clampAckSeq('group.ack_messages', 'msg_seq', ns, contig);
1467
- client._withBackgroundRpc(() => client._ackGroupV2(groupId, ackSeq))
1468
- .catch((e) => { client._clientLog.debug(`group recall auto-ack failed: group=${groupId} ${formatDeliveryError(e)}`); });
1469
- }
1470
- if (contigAfter !== contigBefore)
1471
- this.persistSeq(ns);
1472
- void published;
1473
- return;
1474
- }
1475
- const published = encryptedPush
1476
- ? await client._publishEncryptedPushMessage('group.message_created', 'group.message_undecryptable', ns, seq, msg, true)
1477
- : await this.publishOrderedMessage('group.message_created', ns, seq, msg);
1478
- const contigAfter = client._seqTracker.getContiguousSeq(ns);
1479
- const needPull = Number(seq) > contigAfter && !published;
1480
- if (needPull && client._sessionOptions?.background_sync !== false) {
1481
- client._clientLog.debug(`group message seq gap detected: group=${groupId}, seq=${seq}, contiguous=${contigAfter}`);
1565
+ if (Number(seq) > client._seqTracker.getContiguousSeq(ns) && client._sessionOptions?.background_sync !== false) {
1566
+ client._clientLog.debug(`group push triggering Forward: group=${groupId}, seq=${seq}, contiguous=${client._seqTracker.getContiguousSeq(ns)}`);
1482
1567
  this.fillGroupGap(groupId).catch((exc) => client._clientLog.warn(`background gap fill trigger failed: ${formatDeliveryError(exc)}`));
1483
1568
  }
1484
- const contig = client._seqTracker.getContiguousSeq(ns);
1485
- if (contig > 0) {
1486
- const maxSeen = client._seqTracker.getMaxSeenSeq(ns);
1487
- const ackSeq = this.clampAckSeq('group.ack_messages', 'msg_seq', ns, contig);
1488
- client._clientLog.debug(`group push auto-ack send: group=${groupId}, ns=${ns}, seq=${ackSeq}, contiguous=${contig}, max_seen=${maxSeen}`);
1489
- client._withBackgroundRpc(() => client._ackGroupV2(groupId, ackSeq))
1490
- .then(() => { client._clientLog.debug(`group push auto-ack ok: group=${groupId}, seq=${ackSeq}`); })
1491
- .catch((e) => { client._clientLog.debug(`group message auto-ack failed: group=${groupId} ${formatDeliveryError(e)}`); });
1492
- }
1493
- if (contigAfter !== contigBefore)
1494
- this.persistSeq(ns);
1495
- if (encryptedPush)
1496
- return;
1569
+ return;
1497
1570
  }
1498
1571
  else {
1572
+ if (this.isSelfSentGroupMessage(msg))
1573
+ return;
1499
1574
  if (encryptedPush) {
1500
1575
  await client._publishEncryptedPushMessage('group.message_created', 'group.message_undecryptable', '', seq ?? 0, msg, true);
1501
1576
  return;
@@ -1507,7 +1582,7 @@ export class MessageDeliveryEngine {
1507
1582
  const client = this.runtime.client;
1508
1583
  let groupId = String(notification.group_id ?? '').trim();
1509
1584
  if (!groupId) {
1510
- await client._publishAppEvent('group.message_created', notification);
1585
+ await client._publishAppEvent('group.message_created', notification, 'push');
1511
1586
  return;
1512
1587
  }
1513
1588
  if (client._sessionOptions?.background_sync === false) {
@@ -1657,8 +1732,10 @@ export class MessageDeliveryEngine {
1657
1732
  }
1658
1733
  }
1659
1734
  client._gapFillDone.set(dedupKey, Date.now());
1735
+ this.syncStarted(ns);
1660
1736
  let filled = 0;
1661
1737
  let continuationAfterSeq = 0;
1738
+ let failed = false;
1662
1739
  try {
1663
1740
  let nextAfterSeq = afterSeq;
1664
1741
  const maxPages = singlePage ? 1 : 100;
@@ -1689,9 +1766,9 @@ export class MessageDeliveryEngine {
1689
1766
  return;
1690
1767
  const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
1691
1768
  const eventObjects = events.filter((evt) => isJsonObject(evt));
1692
- if (eventObjects.length > 0) {
1693
- client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq);
1694
- }
1769
+ client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq, Number.isSafeInteger(result.contiguous_seq)
1770
+ ? result.contiguous_seq
1771
+ : undefined);
1695
1772
  const cursor = isJsonObject(result.cursor)
1696
1773
  ? result.cursor
1697
1774
  : null;
@@ -1710,6 +1787,7 @@ export class MessageDeliveryEngine {
1710
1787
  }
1711
1788
  const eventSeqs = [];
1712
1789
  let hasDissolvedEvent = false;
1790
+ let publishedEventCount = 0;
1713
1791
  for (const evt of eventObjects) {
1714
1792
  const eventSeq = Number(evt.event_seq ?? 0);
1715
1793
  if (Number.isFinite(eventSeq) && eventSeq > 0)
@@ -1729,10 +1807,12 @@ export class MessageDeliveryEngine {
1729
1807
  }
1730
1808
  }
1731
1809
  if (Number.isFinite(eventSeq) && eventSeq > 0 && !client._pushedSeqs.get(ns)?.has(eventSeq)) {
1732
- this.enqueueOrderedMessage(ns, 'group.changed', eventSeq, evt);
1810
+ this.enqueueOrderedMessage(ns, 'group.changed', eventSeq, evt, 'pull');
1811
+ publishedEventCount += 1;
1733
1812
  }
1734
1813
  }
1735
- filled += 1;
1814
+ if (et !== 'group.message_created')
1815
+ filled += 1;
1736
1816
  }
1737
1817
  const ackContig = client._seqTracker.getContiguousSeq(ns);
1738
1818
  await this.drainOrderedMessages(ns);
@@ -1759,11 +1839,23 @@ export class MessageDeliveryEngine {
1759
1839
  client._clientLog.debug(`group event auto-ack failed: group=${groupId} ${formatDeliveryError(e)}`);
1760
1840
  }
1761
1841
  }
1842
+ this.syncProgress(ns, {
1843
+ rawCount: eventObjects.length,
1844
+ pulledCount: publishedEventCount,
1845
+ remaining: result.remaining,
1846
+ hasMore: typeof result.has_more === 'boolean'
1847
+ ? Boolean(result.has_more) : undefined,
1848
+ currentSeq: ackContig,
1849
+ targetSeq: Math.max(Number(cursor?.latest_seq ?? 0), Number(result.latest_event_seq ?? 0)),
1850
+ });
1762
1851
  const nextAfter = Math.max(eventSeqs.length > 0 ? Math.max(...eventSeqs) : nextAfterSeq, nextAfterSeq);
1763
- if (singlePage && result.has_more === true && nextAfter > nextAfterSeq) {
1852
+ const pageAdvanced = eventObjects.length > 0 && nextAfter > nextAfterSeq;
1853
+ const needsMore = result.has_more === true
1854
+ || client._seqTracker.getMaxSeenSeq(ns) > nextAfter;
1855
+ if (singlePage && pageAdvanced && needsMore) {
1764
1856
  continuationAfterSeq = nextAfter;
1765
1857
  }
1766
- if (eventObjects.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
1858
+ if (!pageAdvanced || !needsMore)
1767
1859
  break;
1768
1860
  nextAfterSeq = nextAfter;
1769
1861
  }
@@ -1773,6 +1865,7 @@ export class MessageDeliveryEngine {
1773
1865
  client._clientLog.debug(`group event gap fill done: group=${groupId}, after_seq=${afterSeq}, filled=${filled}`);
1774
1866
  }
1775
1867
  catch (exc) {
1868
+ failed = true;
1776
1869
  client._clientLog.warn(`group event gap fill failed: ${formatDeliveryError(exc)}`);
1777
1870
  }
1778
1871
  finally {
@@ -1789,6 +1882,8 @@ export class MessageDeliveryEngine {
1789
1882
  else if (!singlePage && filled > 0 && client._seqTracker.getContiguousSeq(ns) > afterSeq) {
1790
1883
  void this.fillGroupEventGap(groupId);
1791
1884
  }
1885
+ if (!client._rpcPipeline)
1886
+ this.syncStopped(failed ? 'aborted' : 'pull_drained');
1792
1887
  }
1793
1888
  }
1794
1889
  enqueueOnlineUnreadEventHint(data) {
@@ -1823,93 +1918,19 @@ export class MessageDeliveryEngine {
1823
1918
  this.enqueueOnlineUnreadEventHint(data);
1824
1919
  return;
1825
1920
  }
1826
- if (this.isSelfJoinGroupChanged(data)) {
1827
- const contig = client._seqTracker.getContiguousSeq(ns);
1828
- const maxSeen = client._seqTracker.getMaxSeenSeq(ns);
1829
- if (contig === 0 && maxSeen === 0 && eventSeq > 1) {
1830
- client._clientLog.debug(`group.changed self-join baseline: group=${groupId}, event_seq=${eventSeq}, baseline=${eventSeq - 1}`);
1831
- client._seqTracker.forceContiguousSeq(ns, eventSeq - 1);
1832
- }
1833
- }
1834
1921
  const contigBefore = client._seqTracker.getContiguousSeq(ns);
1835
1922
  if (eventSeq <= contigBefore || client._pushedSeqs.get(ns)?.has(eventSeq)) {
1836
1923
  client._clientLog.debug(`group.changed skipped duplicate/stale: group=${groupId}, event_seq=${eventSeq}, contiguous=${contigBefore}`);
1837
- this.fireGroupEventAck(groupId, Math.min(eventSeq, contigBefore > 0 ? contigBefore : eventSeq), 'covered push');
1838
1924
  return;
1839
1925
  }
1840
- this.enqueueOrderedMessage(ns, 'group.changed', eventSeq, data);
1841
- needPull = client._seqTracker.onMessageSeq(ns, eventSeq);
1842
- const ackContig = client._seqTracker.getContiguousSeq(ns);
1843
- await this.drainOrderedMessages(ns);
1844
- if (ackContig > 0 && ackContig !== contigBefore) {
1845
- if (data.action !== 'dissolved')
1846
- this.persistSeq(ns);
1847
- client._transport.call('group.ack_events', {
1848
- group_id: groupId,
1849
- event_seq: ackContig,
1850
- device_id: client._deviceId,
1851
- slot_id: client._slotId,
1852
- }, undefined, undefined, true).catch((e) => {
1853
- client._clientLog.debug(`group event push auto-ack failed: group=${groupId} ${formatDeliveryError(e)}`);
1854
- });
1855
- }
1856
- if (needPull && groupId && !data._from_gap_fill && client._sessionOptions?.background_sync !== false) {
1926
+ client._seqTracker.updateMaxSeen(ns, eventSeq);
1927
+ needPull = eventSeq > contigBefore;
1928
+ if (needPull && !data._from_gap_fill && client._sessionOptions?.background_sync !== false) {
1857
1929
  this.fillGroupEventGap(groupId).catch((exc) => {
1858
1930
  client._clientLog.warn(`background gap fill trigger failed: ${formatDeliveryError(exc)}`);
1859
1931
  });
1860
1932
  }
1861
1933
  }
1862
- fireGroupEventAck(groupId, eventSeq, reason) {
1863
- const client = this.runtime.client;
1864
- const gid = String(groupId ?? '').trim();
1865
- if (!gid)
1866
- return;
1867
- const ns = `group_event:${gid}`;
1868
- const ackSeq = this.clampAckSeq('group.ack_events', 'event_seq', ns, Number(eventSeq) || 0);
1869
- if (ackSeq <= 0)
1870
- return;
1871
- const params = {
1872
- group_id: gid,
1873
- event_seq: ackSeq,
1874
- device_id: client._deviceId,
1875
- slot_id: client._slotId,
1876
- _rpc_background: true,
1877
- };
1878
- try {
1879
- Promise.resolve(client.call('group.ack_events', params)).catch((e) => {
1880
- client._clientLog.debug(`group event ${reason} ack failed: group=${gid} ${formatDeliveryError(e)}`);
1881
- });
1882
- }
1883
- catch (e) {
1884
- client._clientLog.debug(`group event ${reason} ack failed: group=${gid} ${formatDeliveryError(e)}`);
1885
- }
1886
- }
1887
- fireGroupV2Ack(groupId, upToSeq, reason, groupAid = '') {
1888
- const client = this.runtime.client;
1889
- const gid = String(groupId ?? '').trim();
1890
- if (!gid)
1891
- return;
1892
- const ns = `group:${gid}`;
1893
- const ackSeq = this.clampAckSeq('group.v2.ack', 'up_to_seq', ns, Number(upToSeq) || 0);
1894
- if (ackSeq <= 0)
1895
- return;
1896
- const generation = this.inlineAckGeneration;
1897
- try {
1898
- Promise.resolve(client.call('group.v2.ack', {
1899
- group_id: gid,
1900
- ...(groupAid ? { group_aid: groupAid } : {}),
1901
- up_to_seq: ackSeq,
1902
- _rpc_background: true,
1903
- })).then((result) => {
1904
- this.onGroupV2AckResult(gid, result, ackSeq, generation);
1905
- }).catch((e) => {
1906
- client._clientLog.debug(`group.v2 ${reason} ack failed: group=${gid} ${formatDeliveryError(e)}`);
1907
- });
1908
- }
1909
- catch (e) {
1910
- client._clientLog.debug(`group.v2 ${reason} ack failed: group=${gid} ${formatDeliveryError(e)}`);
1911
- }
1912
- }
1913
1934
  isSelfJoinGroupChanged(data) {
1914
1935
  const action = String(data.action ?? '').trim();
1915
1936
  if (!['member_added', 'joined', 'join_approved', 'invite_code_used'].includes(action))
@@ -2206,15 +2227,17 @@ export class MessageDeliveryEngine {
2206
2227
  const state = this.syncState(ns);
2207
2228
  const needsForward = state.ack < state.tail - 1;
2208
2229
  const backgroundEnabled = client._sessionOptions?.background_sync !== false;
2209
- // Tail 期间到达的新 Push 可能只更新 maxSeen 而没有扩展 Tail 窗口。
2230
+ // Tail 期间到达的新 Push 只记录在 pending Pull 上界,不写入 SeqTracker。
2210
2231
  const maxSeen = Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? 0);
2211
- const tailMissedDuringPull = tailHead !== null && maxSeen > state.head;
2232
+ const pendingUpper = Number(client._pendingP2pPullUpper?.get?.(ns) ?? 0);
2233
+ const observedUpper = Math.max(maxSeen, pendingUpper);
2234
+ const tailMissedDuringPull = tailHead !== null && observedUpper > state.head;
2212
2235
  const tailForward = tailHead !== null && (needsForward || tailMissedDuringPull);
2213
2236
  const backgroundWindowForward = (order.length === 1 && order[0] === 'forward' && !headForward)
2214
2237
  || (order.length === 1 && order[0] === 'tail' && tailHead === null && needsForward);
2215
2238
  const foregroundForward = headForward || tailForward;
2216
2239
  if (foregroundForward || backgroundEnabled && backgroundWindowForward) {
2217
- const forwardTarget = Math.max(headForward ? pushSeq : 0, tailForward || backgroundWindowForward ? state.tail - 1 : 0, tailMissedDuringPull ? maxSeen : 0);
2240
+ const forwardTarget = Math.max(headForward ? pushSeq : 0, tailForward || backgroundWindowForward ? state.tail - 1 : 0, tailMissedDuringPull ? observedUpper : 0);
2218
2241
  const requiredPages = Math.max(1, Math.ceil(Math.max(0, forwardTarget - state.ack) / pageLimit));
2219
2242
  result = await run({
2220
2243
  after_seq: state.ack,
@@ -2282,13 +2305,15 @@ export class MessageDeliveryEngine {
2282
2305
  const needsForward = state.ack < state.tail - 1;
2283
2306
  const backgroundEnabled = client._sessionOptions?.background_sync !== false;
2284
2307
  const maxSeen = Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? 0);
2285
- const tailMissedDuringPull = tailHead !== null && maxSeen > state.head;
2308
+ const pendingUpper = this.pendingPullUpper(ns);
2309
+ const observedUpper = Math.max(maxSeen, pendingUpper);
2310
+ const tailMissedDuringPull = tailHead !== null && observedUpper > state.head;
2286
2311
  const tailForward = tailHead !== null && (needsForward || tailMissedDuringPull);
2287
2312
  const backgroundWindowForward = (order.length === 1 && order[0] === 'forward' && !headForward)
2288
2313
  || (order.length === 1 && order[0] === 'tail' && tailHead === null && needsForward);
2289
2314
  const foregroundForward = headForward || tailForward;
2290
2315
  if (foregroundForward || backgroundEnabled && backgroundWindowForward) {
2291
- const forwardTarget = Math.max(headForward ? pushSeq : 0, tailForward || backgroundWindowForward ? state.tail - 1 : 0, tailMissedDuringPull ? maxSeen : 0);
2316
+ const forwardTarget = Math.max(headForward ? pushSeq : 0, tailForward || backgroundWindowForward ? state.tail - 1 : 0, tailMissedDuringPull ? observedUpper : 0);
2292
2317
  const requiredPages = Math.max(1, Math.ceil(Math.max(0, forwardTarget - state.ack) / pageLimit));
2293
2318
  result = await run({
2294
2319
  after_seq: state.ack,
@@ -2898,7 +2923,7 @@ export class MessageDeliveryEngine {
2898
2923
  this.enqueueOnlineUnreadP2pHint(pushData);
2899
2924
  return;
2900
2925
  }
2901
- const inlinePresent = Object.prototype.hasOwnProperty.call(pushData, 'inline_message');
2926
+ const inlinePresent = hasNonEmptyInlineMessage(pushData.inline_message);
2902
2927
  const pushSeq = this.realtimeHeadSeq(rawPushSeq, inlinePresent);
2903
2928
  if (pushSeq === null) {
2904
2929
  if (inlinePresent) {
@@ -2929,7 +2954,8 @@ export class MessageDeliveryEngine {
2929
2954
  client._seqTracker.updateMaxSeen(ns, pushSeq);
2930
2955
  if (pushSeq <= before.ack && before.ack >= before.tail - 1) {
2931
2956
  client._clientLog.debug(`V2 P2P duplicate push ignored: seq=${pushSeq} A/T/H=${JSON.stringify(before)}`);
2932
- this.retryPendingP2PInlineAck(ns, pushSeq, 'covered inline push');
2957
+ if (inlinePresent)
2958
+ this.retryPendingP2PInlineAck(ns, pushSeq, 'covered inline push');
2933
2959
  return;
2934
2960
  }
2935
2961
  const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
@@ -3002,7 +3028,7 @@ export class MessageDeliveryEngine {
3002
3028
  client._logMessageDebug('server-push', '_raw.group.v2.message_created', 'group.message_created', d);
3003
3029
  const groupId = typeof d.group_id === 'string' ? d.group_id.trim() : '';
3004
3030
  const rawSeq = d.seq;
3005
- const inlinePresent = Object.prototype.hasOwnProperty.call(d, 'inline_message');
3031
+ const inlinePresent = hasNonEmptyInlineMessage(d.inline_message);
3006
3032
  const seq = this.realtimeHeadSeq(rawSeq, inlinePresent);
3007
3033
  if (!groupId) {
3008
3034
  client._clientLog.debug(`_onRawGroupV2MessageCreated skipped: group=${groupId || '<empty>'}, seq=${String(d.seq ?? '')}`);
@@ -3047,12 +3073,9 @@ export class MessageDeliveryEngine {
3047
3073
  if (seq <= before.ack && before.ack >= before.tail - 1) {
3048
3074
  client._clientLog.debug(`V2 group duplicate push ignored: group=${groupId} seq=${seq} A/T/H=${JSON.stringify(before)}`);
3049
3075
  const pending = this.pendingGroupInlineAcks?.get(ns);
3050
- if (pending?.seq === seq) {
3076
+ if (inlinePresent && pending?.seq === seq) {
3051
3077
  this.retryPendingGroupInlineAck(ns, seq, 'covered inline push');
3052
3078
  }
3053
- else if (!inlinePresent) {
3054
- this.fireGroupV2Ack(groupId, seq, 'covered push');
3055
- }
3056
3079
  return;
3057
3080
  }
3058
3081
  const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
@@ -3602,44 +3625,18 @@ export class MessageDeliveryEngine {
3602
3625
  }
3603
3626
  return true;
3604
3627
  }
3605
- async publishOrderedGroupRecall(ns, seq, message) {
3606
- const client = this.runtime.client;
3607
- this.ensurePullOperationCurrent();
3608
- const seqNum = Number(seq);
3609
- if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0) {
3610
- await this.publishGroupRecallTombstone(ns.replace(/^group:/, ''), seq, message);
3611
- this.ensurePullOperationCurrent();
3612
- return true;
3613
- }
3614
- // 撤回 tombstone 也占 seq:推进 contiguous 并标记已发布,确保 ack 正常推进、不留空洞。
3615
- if (client._pushedSeqs.get(ns)?.has(seqNum)) {
3616
- await this.publishGroupRecallTombstone(ns.replace(/^group:/, ''), seq, message);
3617
- return false;
3618
- }
3619
- client._seqTracker.onMessageSeq(ns, seqNum);
3620
- await this.publishGroupRecallTombstone(ns.replace(/^group:/, ''), seq, message);
3621
- this.ensurePullOperationCurrent();
3622
- this.markPublishedSeq(ns, seqNum);
3623
- client._markOrderedSeqDelivered?.(ns, seqNum);
3624
- await this.drainOrderedMessages(ns);
3625
- if (!client._pendingOrderedMsgs.get(ns)) {
3626
- this.ensurePullOperationCurrent();
3627
- await this.saveSeqTrackerState();
3628
- }
3629
- return true;
3630
- }
3631
- async publishPulledMessage(event, ns, seq, payload, persist = true) {
3628
+ async publishPulledMessage(event, ns, seq, payload, persist = true, source = 'pull') {
3632
3629
  const client = this.runtime.client;
3633
3630
  this.ensurePullOperationCurrent();
3634
3631
  const seqNum = Number(seq);
3635
3632
  if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0 || !ns) {
3636
3633
  client._clientLog.debug(`publish pulled direct(no-seq): event=${event}, ns=${ns || '<none>'}, seq=${String(seq)}`);
3637
3634
  if (event === 'message.recalled') {
3638
- const published = await this.publishMessageRecallTombstone(seq, payload);
3635
+ const published = await this.publishMessageRecallTombstone(seq, payload, source);
3639
3636
  this.ensurePullOperationCurrent();
3640
3637
  return published;
3641
3638
  }
3642
- const published = client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, 'pull'));
3639
+ const published = client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, source));
3643
3640
  if (isPromiseLike(published))
3644
3641
  await published;
3645
3642
  this.ensurePullOperationCurrent();
@@ -3653,27 +3650,27 @@ export class MessageDeliveryEngine {
3653
3650
  client._pendingOrderedMsgs.delete(ns);
3654
3651
  return false;
3655
3652
  }
3656
- await this.drainOrderedMessages(ns, seqNum, true, persist, 'pull');
3653
+ await this.drainOrderedMessages(ns, seqNum, true, persist, source);
3657
3654
  this.ensurePullOperationCurrent();
3658
3655
  queue?.delete(seqNum);
3659
3656
  if (queue && queue.size === 0)
3660
3657
  client._pendingOrderedMsgs.delete(ns);
3661
3658
  if (event === 'message.recalled') {
3662
- const recallPublished = await this.publishMessageRecallTombstone(seqNum, payload);
3659
+ const recallPublished = await this.publishMessageRecallTombstone(seqNum, payload, source);
3663
3660
  this.ensurePullOperationCurrent();
3664
3661
  this.markPublishedSeq(ns, seqNum);
3665
3662
  client._clientLog.debug(`publish pulled delivered: event=${event}, ns=${ns}, seq=${seqNum}`);
3666
- await this.drainOrderedMessages(ns, undefined, true, persist, 'pull');
3663
+ await this.drainOrderedMessages(ns, undefined, true, persist, source);
3667
3664
  this.ensurePullOperationCurrent();
3668
3665
  return recallPublished;
3669
3666
  }
3670
- const published = client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, 'pull'));
3667
+ const published = client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, source));
3671
3668
  if (isPromiseLike(published))
3672
3669
  await published;
3673
3670
  this.ensurePullOperationCurrent();
3674
3671
  this.markPublishedSeq(ns, seqNum);
3675
3672
  client._clientLog.debug(`publish pulled delivered: event=${event}, ns=${ns}, seq=${seqNum}`);
3676
- await this.drainOrderedMessages(ns, undefined, true, persist, 'pull');
3673
+ await this.drainOrderedMessages(ns, undefined, true, persist, source);
3677
3674
  this.ensurePullOperationCurrent();
3678
3675
  return true;
3679
3676
  }