@agentunion/fastaun-browser 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.
Files changed (44) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/_packed_docs/CHANGELOG.md +62 -0
  3. package/_packed_docs/INDEX.md +3 -3
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +1 -1
  5. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +21 -3
  6. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +59 -0
  7. package/_packed_docs/sdk/09-group-rpc-manual.md +29 -2
  8. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +3 -2
  9. package/_packed_docs/sdk/INDEX.md +2 -1
  10. package/dist/bundle.js +1145 -381
  11. package/dist/client/delivery.d.ts +12 -4
  12. package/dist/client/delivery.d.ts.map +1 -1
  13. package/dist/client/delivery.js +341 -273
  14. package/dist/client/delivery.js.map +1 -1
  15. package/dist/client/group-state.d.ts.map +1 -1
  16. package/dist/client/group-state.js +1 -0
  17. package/dist/client/group-state.js.map +1 -1
  18. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  19. package/dist/client/rpc-pipeline.js +38 -13
  20. package/dist/client/rpc-pipeline.js.map +1 -1
  21. package/dist/client/v2-e2ee.d.ts +3 -1
  22. package/dist/client/v2-e2ee.d.ts.map +1 -1
  23. package/dist/client/v2-e2ee.js +211 -75
  24. package/dist/client/v2-e2ee.js.map +1 -1
  25. package/dist/client.d.ts +15 -0
  26. package/dist/client.d.ts.map +1 -1
  27. package/dist/client.js +402 -20
  28. package/dist/client.js.map +1 -1
  29. package/dist/facades.d.ts +7 -0
  30. package/dist/facades.d.ts.map +1 -1
  31. package/dist/facades.js +44 -0
  32. package/dist/facades.js.map +1 -1
  33. package/dist/seq-tracker.d.ts +2 -2
  34. package/dist/seq-tracker.d.ts.map +1 -1
  35. package/dist/seq-tracker.js +11 -7
  36. package/dist/seq-tracker.js.map +1 -1
  37. package/dist/tools/cross-sdk-agent.js +50 -12
  38. package/dist/tools/cross-sdk-agent.js.map +1 -1
  39. package/dist/transport.d.ts.map +1 -1
  40. package/dist/transport.js +162 -15
  41. package/dist/transport.js.map +1 -1
  42. package/dist/version.d.ts +1 -1
  43. package/dist/version.js +1 -1
  44. package/package.json +1 -1
@@ -58,11 +58,35 @@ function positiveSafeSequenceHint(value) {
58
58
  const parsed = Number(value);
59
59
  return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 0;
60
60
  }
61
+ function hasNonEmptyInlineMessage(value) {
62
+ return isJsonObject(value)
63
+ && Object.keys(value).length > 0;
64
+ }
61
65
  function nonNegativeSafeSequenceHint(value) {
62
66
  if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
63
67
  return null;
64
68
  return value;
65
69
  }
70
+ function canonicalDeliverySource(source) {
71
+ switch (String(source ?? '').trim()) {
72
+ case 'group-push':
73
+ case 'ordered':
74
+ case 'legacy':
75
+ return 'push';
76
+ case 'tail':
77
+ case 'pull-drained':
78
+ case 'pull_drained':
79
+ return 'pull';
80
+ case 'inline-push':
81
+ return 'inline_push';
82
+ case 'pending-retry':
83
+ return 'pending_retry';
84
+ case '':
85
+ return 'direct';
86
+ default:
87
+ return String(source).trim();
88
+ }
89
+ }
66
90
  function deduplicatePlainForwardPageMessages(value, afterSeq) {
67
91
  if (!Array.isArray(value))
68
92
  return [];
@@ -95,6 +119,9 @@ function p2pAppEventFromPlainPullMessage(message) {
95
119
  }
96
120
  export class MessageDeliveryEngine {
97
121
  runtime;
122
+ syncRun = 0;
123
+ syncingNamespaces = new Set();
124
+ syncSessions = new Map();
98
125
  pendingPullDeliveryChanges = null;
99
126
  deliveryGeneration = 0;
100
127
  realtimeTailResults = null;
@@ -116,6 +143,7 @@ export class MessageDeliveryEngine {
116
143
  this.runtime = runtime;
117
144
  }
118
145
  resetInlineAckState() {
146
+ this.syncStopped('aborted');
119
147
  this.inlineGeneration += 1;
120
148
  this.deliveryGeneration += 1;
121
149
  this.pendingPullDeliveryChanges = null;
@@ -131,6 +159,8 @@ export class MessageDeliveryEngine {
131
159
  this.pendingP2PInlineAcks = null;
132
160
  this.pendingGroupInlineAcks = null;
133
161
  this.pendingGroupInlineAckNamespaces = null;
162
+ this.syncingNamespaces.clear();
163
+ this.syncSessions.clear();
134
164
  }
135
165
  isInlineGenerationCurrent(generation) {
136
166
  return generation === this.inlineGeneration;
@@ -163,6 +193,95 @@ export class MessageDeliveryEngine {
163
193
  endRealtimeSync(ns) {
164
194
  this.realtimeSyncing?.delete(ns);
165
195
  }
196
+ onPullStarted(ns) {
197
+ if (!ns || this.syncingNamespaces.has(ns))
198
+ return;
199
+ if (this.syncingNamespaces.size === 0) {
200
+ this.syncRun += 1;
201
+ this.syncSessions.clear();
202
+ this.runtime.client._dispatcher?.enqueue?.('sync.started', {
203
+ run_id: this.syncRun,
204
+ source: 'pull',
205
+ syncing: true,
206
+ namespaces_pending: 1,
207
+ received_total: 0,
208
+ namespace: ns,
209
+ started_at: Date.now(),
210
+ });
211
+ }
212
+ this.syncingNamespaces.add(ns);
213
+ this.syncSessions.set(ns, { pages: 0, raw: 0, pulled: 0 });
214
+ }
215
+ onPullPage(ns, pulledCount, hasMore, maxSeen, remaining, rawCount = pulledCount, currentSeq, targetSeq) {
216
+ this.onPullStarted(ns);
217
+ if (!ns)
218
+ return;
219
+ const session = this.syncSessions.get(ns) ?? { pages: 0, raw: 0, pulled: 0 };
220
+ session.pages += 1;
221
+ session.raw += Math.max(0, rawCount);
222
+ session.pulled += Math.max(0, pulledCount);
223
+ this.syncSessions.set(ns, session);
224
+ const received = session.pulled;
225
+ const progress = {
226
+ run_id: this.syncRun,
227
+ source: 'pull',
228
+ namespace: ns,
229
+ page: session.pages,
230
+ page_raw_count: Math.max(0, rawCount),
231
+ page_pulled_count: Math.max(0, pulledCount),
232
+ pulled_count: received,
233
+ received_total: received,
234
+ batch_size: Math.max(0, pulledCount),
235
+ estimated_remaining: hasMore ? undefined : 0,
236
+ max_seen: maxSeen,
237
+ has_more: hasMore,
238
+ };
239
+ if (typeof currentSeq === 'number' && Number.isSafeInteger(currentSeq) && currentSeq >= 0) {
240
+ progress.current_seq = currentSeq;
241
+ }
242
+ if (typeof targetSeq === 'number' && Number.isSafeInteger(targetSeq) && targetSeq >= 0) {
243
+ progress.target_seq = targetSeq;
244
+ }
245
+ if (typeof remaining === 'number' && Number.isSafeInteger(remaining) && remaining >= 0) {
246
+ progress.remaining = remaining;
247
+ progress.estimated_remaining = progress.remaining;
248
+ }
249
+ this.runtime.client._dispatcher?.enqueue?.('sync.progress', progress);
250
+ }
251
+ onPullAborted() {
252
+ this.syncStopped('aborted');
253
+ }
254
+ syncStopped(reason = 'pull_drained') {
255
+ if (this.syncingNamespaces.size === 0)
256
+ return;
257
+ const sessions = [...this.syncSessions.entries()];
258
+ const receivedTotal = sessions.reduce((total, [, session]) => total + session.pulled, 0);
259
+ this.syncingNamespaces.clear();
260
+ this.syncSessions.clear();
261
+ const payload = {
262
+ run_id: this.syncRun,
263
+ source: 'pull',
264
+ syncing: false,
265
+ namespaces_pending: 0,
266
+ received_total: receivedTotal,
267
+ reason,
268
+ stopped_at: Date.now(),
269
+ };
270
+ if (sessions.length === 1) {
271
+ const [namespace, session] = sessions[0];
272
+ payload.namespace = namespace;
273
+ payload.pages = session.pages;
274
+ payload.pulled_count = session.pulled;
275
+ }
276
+ else if (sessions.length > 1) {
277
+ payload.namespaces = sessions.map(([namespace]) => namespace);
278
+ payload.pages = sessions.reduce((total, [, session]) => total + session.pages, 0);
279
+ payload.pulled_count = receivedTotal;
280
+ }
281
+ this.runtime.client._dispatcher?.enqueue?.('sync.stopped', {
282
+ ...payload,
283
+ });
284
+ }
166
285
  hasPendingPull(ns) {
167
286
  const pending = ns.startsWith('p2p:') ? this.pendingP2pPullUpper : this.pendingGroupPullUpper;
168
287
  return (pending?.get(ns) ?? 0) > 0;
@@ -176,7 +295,7 @@ export class MessageDeliveryEngine {
176
295
  const pipeline = this.runtime.client._rpcPipeline;
177
296
  if (pipeline?.hasAnyPullActivity?.() === true)
178
297
  return;
179
- void this.flushPullDeliveryChanges();
298
+ void this.flushPullDeliveryChanges().finally(() => this.syncStopped('pull_drained'));
180
299
  }
181
300
  deliveryChangeNamespace(event, payload, ns = '') {
182
301
  if (event === 'message.received')
@@ -236,7 +355,11 @@ export class MessageDeliveryEngine {
236
355
  const changes = this.deliveryChangesPayload(batch.changes);
237
356
  if (changes.length === 0)
238
357
  return;
239
- this.runtime.client._dispatcher.enqueue('delivery.changed', { trigger: batch.trigger, changes });
358
+ this.runtime.client._dispatcher.enqueue('delivery.changed', {
359
+ trigger: batch.trigger,
360
+ source: batch.trigger,
361
+ changes,
362
+ });
240
363
  }
241
364
  async flushPullDeliveryChanges() {
242
365
  const generation = this.deliveryGeneration;
@@ -249,7 +372,11 @@ export class MessageDeliveryEngine {
249
372
  const changes = this.deliveryChangesPayload(pending);
250
373
  if (changes.length === 0)
251
374
  return;
252
- this.runtime.client._dispatcher.enqueue('delivery.changed', { trigger: 'pull_drained', changes });
375
+ this.runtime.client._dispatcher.enqueue('delivery.changed', {
376
+ trigger: 'pull_drained',
377
+ source: 'pull',
378
+ changes,
379
+ });
253
380
  }
254
381
  recordPendingPull(ns, seq) {
255
382
  if (!ns || !Number.isSafeInteger(seq) || seq <= 0)
@@ -852,20 +979,28 @@ export class MessageDeliveryEngine {
852
979
  }
853
980
  const message = normalizeGroupMentionMode(rawMessage);
854
981
  if (this.recallEventFromGroupMessage(message)) {
855
- if (await this.publishGroupRecallTombstone(groupId, seq, message)) {
982
+ if (await this.publishGroupRecallTombstone(groupId, seq, message, 'pull')) {
856
983
  this.ensurePullOperationCurrent();
857
984
  this.markPublishedSeq(ns, seq);
858
985
  publishedCount += 1;
859
986
  }
860
987
  }
988
+ else if (this.isSelfSentGroupMessage(message)) {
989
+ this.markPublishedSeq(ns, seq);
990
+ }
861
991
  else if (await this.publishPulledMessage('group.message_created', ns, seq, message, false)) {
862
992
  this.ensurePullOperationCurrent();
863
993
  publishedCount += 1;
864
994
  }
865
995
  }
866
996
  this.ensurePullOperationCurrent();
867
- if (messages.length > 0)
997
+ const contiguousSeq = Number.isSafeInteger(result.contiguous_seq)
998
+ ? result.contiguous_seq
999
+ : undefined;
1000
+ if (contiguousSeq === undefined)
868
1001
  client._seqTracker.onPullResult(ns, messages, afterSeq);
1002
+ else
1003
+ client._seqTracker.onPullResult(ns, messages, afterSeq, contiguousSeq);
869
1004
  const commitTarget = Math.max(client._seqTracker.getContiguousSeq(ns), retentionFloor, visibilityFloor, deferredServerCursor);
870
1005
  if (commitTarget > client._seqTracker.getContiguousSeq(ns)) {
871
1006
  client._seqTracker.forceContiguousSeq(ns, commitTarget);
@@ -919,6 +1054,9 @@ export class MessageDeliveryEngine {
919
1054
  this.ensurePullOperationCurrent();
920
1055
  await this.confirmPlainForwardAck(ns, ackMethod, pendingAckSeq, groupId);
921
1056
  }
1057
+ const remaining = typeof response.remaining === 'number' && Number.isSafeInteger(response.remaining) && response.remaining >= 0
1058
+ ? response.remaining : null;
1059
+ this.onPullPage(ns, publishedCount, response.has_more === true, messages.length > 0 ? Number(messages[messages.length - 1].seq ?? 0) : undefined, remaining, messages.length, client._seqTracker.getContiguousSeq(ns), messages.length > 0 ? Number(messages[messages.length - 1].seq ?? 0) : undefined);
922
1060
  return { rawCount: messages.length, publishedCount };
923
1061
  }
924
1062
  enqueueOrderedMessage(ns, event, seq, payload, source = 'push') {
@@ -941,11 +1079,11 @@ export class MessageDeliveryEngine {
941
1079
  async publishOrderedQueueItem(ns, event, seq, payload, pullResponse = false, source = 'push', batch) {
942
1080
  const client = this.runtime.client;
943
1081
  if (event === 'group.changed' && this.isGroupEventNamespace(ns)) {
944
- await this.publishOrderedGroupChanged(payload);
1082
+ await this.publishOrderedGroupChanged(payload, source);
945
1083
  return;
946
1084
  }
947
1085
  if (event === 'message.recalled') {
948
- await this.publishMessageRecallTombstone(seq, payload);
1086
+ await this.publishMessageRecallTombstone(seq, payload, source);
949
1087
  return;
950
1088
  }
951
1089
  if (pullResponse) {
@@ -954,7 +1092,7 @@ export class MessageDeliveryEngine {
954
1092
  }
955
1093
  await client._publishAppEvent(event, payload, source, ns, batch);
956
1094
  }
957
- async publishOrderedGroupChanged(payload) {
1095
+ async publishOrderedGroupChanged(payload, source = 'ordered') {
958
1096
  const client = this.runtime.client;
959
1097
  if (isJsonObject(payload)) {
960
1098
  const eventPayload = payload;
@@ -966,7 +1104,7 @@ export class MessageDeliveryEngine {
966
1104
  client._cleanupDissolvedGroup?.(groupId);
967
1105
  }
968
1106
  }
969
- await client._publishAppEvent('group.changed', payload);
1107
+ await client._publishAppEvent('group.changed', payload, source);
970
1108
  }
971
1109
  isInstanceScopedMessageEvent(event) {
972
1110
  return event === 'message.received'
@@ -989,18 +1127,27 @@ export class MessageDeliveryEngine {
989
1127
  }
990
1128
  return result;
991
1129
  }
992
- normalizePublishedMessagePayload(event, payload) {
1130
+ normalizePublishedMessagePayload(event, payload, source = 'direct') {
1131
+ let normalized;
993
1132
  if (this.isInstanceScopedMessageEvent(event)) {
994
1133
  payload = this.stripInlineInternalFields(payload);
995
1134
  if (event === 'group.message_created')
996
1135
  payload = normalizeGroupMentionMode(payload);
997
- return this.attachAppMessageEnvelope(this.stripInternalSenderDeviceFields(this.attachCurrentInstanceContext(payload)));
1136
+ normalized = this.attachAppMessageEnvelope(this.stripInternalSenderDeviceFields(this.attachCurrentInstanceContext(payload)));
998
1137
  }
999
- if (this.isGroupScopedEvent(event)) {
1138
+ else if (this.isGroupScopedEvent(event)) {
1000
1139
  payload = this.stripInlineInternalFields(payload);
1001
- return this.attachAppGroupEventEnvelope(this.attachCurrentInstanceContext(payload));
1140
+ normalized = this.attachAppGroupEventEnvelope(this.attachCurrentInstanceContext(payload));
1141
+ }
1142
+ else {
1143
+ normalized = payload;
1002
1144
  }
1003
- return payload;
1145
+ const canonicalSource = canonicalDeliverySource(source);
1146
+ if (canonicalSource === 'direct' || !isJsonObject(normalized))
1147
+ return normalized;
1148
+ if (!this.isInstanceScopedMessageEvent(event) && !this.isGroupScopedEvent(event))
1149
+ return normalized;
1150
+ return { ...normalized, source: canonicalSource };
1004
1151
  }
1005
1152
  stripInlineInternalFields(payload) {
1006
1153
  if (!isJsonObject(payload))
@@ -1269,7 +1416,7 @@ export class MessageDeliveryEngine {
1269
1416
  return `p2p|tombstone:${tombstoneId}`;
1270
1417
  return `p2p|unknown:${Date.now()}:${Math.random()}`;
1271
1418
  }
1272
- async publishMessageRecallTombstone(seq, message) {
1419
+ async publishMessageRecallTombstone(seq, message, source = 'push') {
1273
1420
  const client = this.runtime.client;
1274
1421
  const eventPayload = this.recallEventFromMessage(message);
1275
1422
  if (!eventPayload)
@@ -1290,7 +1437,7 @@ export class MessageDeliveryEngine {
1290
1437
  for (const [oldKey] of drop)
1291
1438
  seen.delete(oldKey);
1292
1439
  }
1293
- await client._publishAppEvent('message.recalled', eventPayload);
1440
+ await client._publishAppEvent('message.recalled', eventPayload, source);
1294
1441
  client._clientLog.debug(`message.recalled published: seq=${String(seq)} ids=${JSON.stringify(eventPayload.message_ids)}`);
1295
1442
  return true;
1296
1443
  }
@@ -1373,7 +1520,7 @@ export class MessageDeliveryEngine {
1373
1520
  return `${normalizedGroupId}|tombstone:${tombstoneId}`;
1374
1521
  return `${normalizedGroupId}|unknown:${Date.now()}:${Math.random()}`;
1375
1522
  }
1376
- async publishGroupRecallTombstone(groupId, seq, message) {
1523
+ async publishGroupRecallTombstone(groupId, seq, message, source = 'push') {
1377
1524
  const client = this.runtime.client;
1378
1525
  const eventPayload = this.recallEventFromGroupMessage(message);
1379
1526
  if (!eventPayload)
@@ -1396,16 +1543,12 @@ export class MessageDeliveryEngine {
1396
1543
  for (const [oldKey] of drop)
1397
1544
  seen.delete(oldKey);
1398
1545
  }
1399
- await client._publishAppEvent('group.message_recalled', eventPayload);
1546
+ await client._publishAppEvent('group.message_recalled', eventPayload, source);
1400
1547
  client._clientLog.debug(`group.message_recalled published: group=${groupId} seq=${String(seq)} ids=${JSON.stringify(eventPayload.message_ids)}`);
1401
1548
  return true;
1402
1549
  }
1403
1550
  async onRawGroupMessageRecalled(data) {
1404
- // 在线 push 是实时通道,与 pull 兜底的双 tombstone 互补。push 携带的 seq 是通知
1405
- // tombstone 的 notice_seq;必须像普通群消息 push 一样推进 seqTracker + markPublished +
1406
- // auto-ack,否则该 seq 在本地 contiguous 序列留洞,后续 pull/reconnect 会重复拉到并
1407
- // 重复处理。publishGroupRecallTombstone 内部再按 (group_id, message_ids) 去重,
1408
- // 确保应用层只回调一次。
1551
+ // inline push 仅作为实时通知,不推进 seqTracker/cursor/ACK。
1409
1552
  const client = this.runtime.client;
1410
1553
  if (!isJsonObject(data))
1411
1554
  return;
@@ -1436,41 +1579,13 @@ export class MessageDeliveryEngine {
1436
1579
  return;
1437
1580
  }
1438
1581
  const ns = `group:${groupId}`;
1439
- if (seqNum > 0) {
1440
- client._seqTracker.updateMaxSeen(ns, seqNum);
1441
- if (client._seqTracker.getContiguousSeq(ns) === seqNum) {
1442
- // 已覆盖(pull 先到并推进过),仍走去重发布兜底,不重复推进 seq。
1443
- await this.publishGroupRecallTombstone(groupId, seq, wrapped);
1444
- return;
1445
- }
1446
- client._repairPushContiguousBound(ns, seqNum, true, '_raw.group.message_recalled');
1447
- }
1448
- // 该 notice_seq 已由 pull 路径处理过(已发布或挂起待发布)时,去重发布兜底后返回。
1449
- const pushed = client._pushedSeqs.get(ns);
1450
- const pending = client._pendingOrderedMsgs.get(ns);
1451
- if (pushed?.has(seqNum) || pending?.has(seqNum)) {
1452
- await this.publishGroupRecallTombstone(groupId, seq, wrapped);
1453
- return;
1454
- }
1455
- const contigBefore = client._seqTracker.getContiguousSeq(ns);
1456
- client._seqTracker.onMessageSeq(ns, seqNum);
1582
+ client._seqTracker.updateMaxSeen(ns, seqNum);
1583
+ if (seqNum > client._seqTracker.getContiguousSeq(ns)
1584
+ && client._sessionOptions?.background_sync !== false)
1585
+ client._safeAsync(this.fillGroupGap(groupId));
1457
1586
  await this.publishGroupRecallTombstone(groupId, seq, wrapped);
1458
- this.markPublishedSeq(ns, seqNum);
1459
- const contig = client._seqTracker.getContiguousSeq(ns);
1460
- if (contig > 0) {
1461
- const ackSeq = this.clampAckSeq('group.ack_messages', 'msg_seq', ns, contig);
1462
- client._transport.call('group.ack_messages', {
1463
- group_id: groupId,
1464
- msg_seq: ackSeq,
1465
- device_id: client._deviceId,
1466
- slot_id: client._slotId,
1467
- _rpc_background: true,
1468
- }).catch((e) => { client._clientLog.warn('group recall auto-ack failed: group=' + groupId, e); });
1469
- }
1470
- if (contig !== contigBefore)
1471
- this.persistSeq(ns);
1472
1587
  }
1473
- async publishAppEvent(event, payload, source = '', ns = '', batch) {
1588
+ async publishAppEvent(event, payload, source = 'direct', ns = '', batch) {
1474
1589
  const client = this.runtime.client;
1475
1590
  const generation = this.deliveryGeneration;
1476
1591
  if ((event === 'message.received' || event === 'group.message_created') && isJsonObject(payload)) {
@@ -1492,14 +1607,21 @@ export class MessageDeliveryEngine {
1492
1607
  client._clientLog.debug(`agent_md etag inject skipped: ${String(exc)}`);
1493
1608
  }
1494
1609
  }
1495
- client._dispatcher.enqueue(event, this.normalizePublishedMessagePayload(event, payload));
1610
+ const canonicalSource = canonicalDeliverySource(source);
1611
+ let normalized = this.normalizePublishedMessagePayload(event, payload, source);
1612
+ if (isJsonObject(normalized)
1613
+ && (this.isInstanceScopedMessageEvent(event) || this.isGroupScopedEvent(event))
1614
+ && canonicalSource !== 'direct') {
1615
+ normalized = { ...normalized, source: canonicalSource };
1616
+ }
1617
+ client._dispatcher.enqueue(event, normalized);
1496
1618
  if (generation !== this.deliveryGeneration)
1497
1619
  return;
1498
- if (source !== 'pull' && source !== 'tail' && source !== 'pending_retry'
1499
- && source !== 'push' && source !== 'inline_push')
1620
+ if (canonicalSource !== 'pull' && canonicalSource !== 'pending_retry'
1621
+ && canonicalSource !== 'push' && canonicalSource !== 'inline_push')
1500
1622
  return;
1501
- if (source === 'push' || source === 'inline_push') {
1502
- const localBatch = batch ?? this.createDeliveryChangeBatch(source);
1623
+ if (canonicalSource === 'push' || canonicalSource === 'inline_push') {
1624
+ const localBatch = batch ?? this.createDeliveryChangeBatch(canonicalSource);
1503
1625
  this.recordDeliveryChange(localBatch.changes, event, payload, ns, generation);
1504
1626
  if (!batch)
1505
1627
  await this.flushRealtimeDeliveryChanges(localBatch);
@@ -1542,6 +1664,21 @@ export class MessageDeliveryEngine {
1542
1664
  }
1543
1665
  return true;
1544
1666
  }
1667
+ isSelfSentGroupMessage(message) {
1668
+ if (!isJsonObject(message))
1669
+ return false;
1670
+ const client = this.runtime.client;
1671
+ const selfAid = String(client._aid ?? '').trim().toLowerCase();
1672
+ const senderAid = String(message.sender_aid ?? message.from_aid ?? message.from ?? '').trim().toLowerCase();
1673
+ if (!selfAid || senderAid !== selfAid)
1674
+ return false;
1675
+ const senderDevice = Object.prototype.hasOwnProperty.call(message, 'sender_device_id')
1676
+ ? message.sender_device_id
1677
+ : message.from_device_id;
1678
+ if (typeof senderDevice !== 'string' || !senderDevice.trim() || !String(client._deviceId ?? '').trim())
1679
+ return false;
1680
+ return senderDevice.trim() === String(client._deviceId).trim();
1681
+ }
1545
1682
  strictTargetString(source, key) {
1546
1683
  if (!Object.prototype.hasOwnProperty.call(source, key)) {
1547
1684
  return { present: false, valid: false, value: '' };
@@ -1611,13 +1748,12 @@ export class MessageDeliveryEngine {
1611
1748
  const ns = isJsonObject(data) && client._aid && data.seq !== undefined
1612
1749
  ? `p2p:${client._aid}`
1613
1750
  : '';
1614
- const seq = isJsonObject(data) && typeof data.seq === 'number' ? data.seq : 0;
1615
- if (client._v2Session && ns && Number.isSafeInteger(seq) && seq > 0
1616
- && this.messageTargetsCurrentInstance(data)) {
1751
+ const seq = isJsonObject(data) ? positiveSafeSequenceHint(data.seq) : 0;
1752
+ if (client._v2Session && ns && seq > 0 && this.messageTargetsCurrentInstance(data)) {
1617
1753
  client._seqTracker.updateMaxSeen(ns, seq);
1618
1754
  }
1619
1755
  const hasInlineObject = isJsonObject(data)
1620
- && isJsonObject(data.inline_message);
1756
+ && hasNonEmptyInlineMessage(data.inline_message);
1621
1757
  const inlineGeneration = hasInlineObject ? this.captureInlineGeneration() : undefined;
1622
1758
  const operation = () => this.processAndPublishMessage(data, inlineGeneration);
1623
1759
  client._safeAsync(client._v2Session && ns && !hasInlineObject
@@ -1634,33 +1770,15 @@ export class MessageDeliveryEngine {
1634
1770
  msg.type = 'message.recalled';
1635
1771
  if (!this.messageTargetsCurrentInstance(msg))
1636
1772
  return;
1637
- const seq = msg.seq;
1638
- if (seq !== undefined && seq !== null && client._aid) {
1773
+ const seq = positiveSafeSequenceHint(msg.seq);
1774
+ if (client._aid && seq > 0) {
1639
1775
  const ns = `p2p:${client._aid}`;
1640
- if (seq > 0)
1641
- client._seqTracker.updateMaxSeen(ns, seq);
1642
- const contigBefore = client._seqTracker.getContiguousSeq(ns);
1643
- const seqNeedsPull = client._seqTracker.onMessageSeq(ns, seq);
1644
- const published = await this.publishOrderedMessage('message.recalled', ns, seq, msg);
1645
- const contigAfter = client._seqTracker.getContiguousSeq(ns);
1646
- if (seqNeedsPull && !published && client._sessionOptions?.background_sync !== false) {
1776
+ client._seqTracker.updateMaxSeen(ns, seq);
1777
+ if (seq > client._seqTracker.getContiguousSeq(ns)
1778
+ && client._sessionOptions?.background_sync !== false)
1647
1779
  client._safeAsync(this.fillP2pGap());
1648
- }
1649
- const contig = client._seqTracker.getContiguousSeq(ns);
1650
- if (contig > 0) {
1651
- const ackSeq = this.clampAckSeq('message.ack', 'seq', ns, contig);
1652
- client._transport.call('message.ack', {
1653
- seq: ackSeq,
1654
- device_id: client._deviceId,
1655
- slot_id: client._slotId,
1656
- _rpc_background: true,
1657
- }).catch((e) => { client._clientLog.warn(`P2P recall auto-ack failed:${String(e)}`); });
1658
- }
1659
- if (contigAfter !== contigBefore)
1660
- this.persistSeq(ns);
1661
- return;
1662
1780
  }
1663
- await this.publishMessageRecallTombstone(seq, msg);
1781
+ await this.publishMessageRecallTombstone(msg.seq, msg);
1664
1782
  }
1665
1783
  async processAndPublishMessage(data, inlineGeneration) {
1666
1784
  const client = this.runtime.client;
@@ -1677,47 +1795,36 @@ export class MessageDeliveryEngine {
1677
1795
  // 新服务端为兼容旧 SDK 仍可能发送 message.received;V2 会话下必须
1678
1796
  // 在旧 seq tracking / ordered publish 之前转入前台 Tail 协调器。
1679
1797
  const legacyPushSeq = positiveSafeSequenceHint(msg.seq);
1680
- const hasInlineField = Object.prototype.hasOwnProperty.call(msg, 'inline_message');
1798
+ const hasInlineField = hasNonEmptyInlineMessage(msg.inline_message);
1681
1799
  if (client._v2Session && client._aid && (legacyPushSeq > 0 || hasInlineField)) {
1682
1800
  await this.processV2P2PNotification(msg, 'v1', inlineGeneration);
1683
1801
  return;
1684
1802
  }
1803
+ if (client._aid && legacyPushSeq > 0) {
1804
+ const ns = `p2p:${client._aid}`;
1805
+ client._seqTracker.updateMaxSeen(ns, legacyPushSeq);
1806
+ if (client._sessionOptions?.background_sync !== false)
1807
+ client._safeAsync(this.fillP2pGap());
1808
+ return;
1809
+ }
1685
1810
  const seq = msg.seq;
1686
1811
  const encryptedPush = client._isEncryptedPushMessage(msg);
1687
- if (seq !== undefined && seq !== null && client._aid) {
1688
- const ns = `p2p:${client._aid}`;
1689
- if (seq > 0)
1690
- client._seqTracker.updateMaxSeen(ns, seq);
1691
- const contigBefore = client._seqTracker.getContiguousSeq(ns);
1692
- const seqNeedsPull = client._seqTracker.onMessageSeq(ns, seq);
1693
- const published = encryptedPush
1694
- ? await client._publishEncryptedPushMessage('message.received', 'message.undecryptable', ns, seq, msg, false)
1695
- : await this.publishOrderedMessage('message.received', ns, seq, msg);
1696
- const contigAfter = client._seqTracker.getContiguousSeq(ns);
1697
- const needPull = seqNeedsPull && !published;
1698
- if (needPull && client._sessionOptions?.background_sync !== false) {
1812
+ const ns = client._aid ? `p2p:${client._aid}` : '';
1813
+ const seqNum = positiveSafeSequenceHint(seq);
1814
+ if (ns && seqNum > 0) {
1815
+ client._seqTracker.updateMaxSeen(ns, seqNum);
1816
+ if (seqNum > client._seqTracker.getContiguousSeq(ns)
1817
+ && client._sessionOptions?.background_sync !== false)
1699
1818
  client._safeAsync(this.fillP2pGap());
1700
- }
1701
- const contig = client._seqTracker.getContiguousSeq(ns);
1702
- if (contig > 0) {
1703
- const ackSeq = this.clampAckSeq('message.ack', 'seq', ns, contig);
1704
- client._transport.call('message.ack', {
1705
- seq: ackSeq,
1706
- device_id: client._deviceId,
1707
- slot_id: client._slotId,
1708
- _rpc_background: true,
1709
- }).catch((e) => { client._clientLog.warn(`P2P auto-ack failed:${String(e)}`); });
1710
- }
1711
- if (contigAfter !== contigBefore)
1712
- this.persistSeq(ns);
1713
- if (encryptedPush)
1714
- return;
1819
+ }
1820
+ if (encryptedPush) {
1821
+ await client._publishEncryptedPushMessage('message.received', 'message.undecryptable', ns, seq ?? 0, msg, false);
1822
+ return;
1823
+ }
1824
+ if (ns && seq !== undefined && seq !== null) {
1825
+ await this.publishOrderedMessage('message.received', ns, seq, msg);
1715
1826
  }
1716
1827
  else {
1717
- if (encryptedPush) {
1718
- await client._publishEncryptedPushMessage('message.received', 'message.undecryptable', '', seq ?? 0, msg, false);
1719
- return;
1720
- }
1721
1828
  await client._publishAppEvent('message.received', msg, 'push');
1722
1829
  }
1723
1830
  }
@@ -1734,7 +1841,7 @@ export class MessageDeliveryEngine {
1734
1841
  _decrypt_error: String(exc),
1735
1842
  };
1736
1843
  client._attachV2EnvelopeMetadataFromSource(safeEvent, data);
1737
- await client._publishAppEvent('message.undecryptable', safeEvent);
1844
+ await client._publishAppEvent('message.undecryptable', safeEvent, 'push');
1738
1845
  }
1739
1846
  }
1740
1847
  }
@@ -1742,15 +1849,14 @@ export class MessageDeliveryEngine {
1742
1849
  const client = this.runtime.client;
1743
1850
  client._clientLog.debug(`_onRawGroupMessageCreated enter: group_id=${data?.group_id ?? '-'} from=${data?.from ?? '-'} seq=${data?.seq ?? '-'}`);
1744
1851
  const groupId = isJsonObject(data) && typeof data.group_id === 'string' ? data.group_id.trim() : '';
1745
- const seq = isJsonObject(data) && typeof data.seq === 'number' ? data.seq : 0;
1746
1852
  const kind = isJsonObject(data) ? String(data.kind ?? '').trim() : '';
1747
- if (client._v2Session && groupId && kind !== 'group.online_unread_hint'
1748
- && Number.isSafeInteger(seq) && seq > 0) {
1749
- client._seqTracker.updateMaxSeen(`group:${groupId}`, seq);
1750
- }
1751
1853
  const ns = groupId ? `group:${groupId}` : '';
1854
+ const seq = isJsonObject(data) ? positiveSafeSequenceHint(data.seq) : 0;
1855
+ if (client._v2Session && ns && kind !== 'group.online_unread_hint' && seq > 0) {
1856
+ client._seqTracker.updateMaxSeen(ns, seq);
1857
+ }
1752
1858
  const hasInlineObject = isJsonObject(data)
1753
- && isJsonObject(data.inline_message);
1859
+ && hasNonEmptyInlineMessage(data.inline_message);
1754
1860
  const inlineGeneration = hasInlineObject ? this.captureInlineGeneration() : undefined;
1755
1861
  const operation = () => this.processAndPublishGroupMessage(data, inlineGeneration);
1756
1862
  client._safeAsync(client._v2Session && ns && !hasInlineObject
@@ -1775,74 +1881,61 @@ export class MessageDeliveryEngine {
1775
1881
  // V2 会话中旧 group.message_created 也只是 Head 通知;必须先 Tail,
1776
1882
  // 不能让 payload 路径直接推进旧 ordered publish 状态。
1777
1883
  const legacyPushSeq = positiveSafeSequenceHint(seq);
1778
- const hasInlineField = Object.prototype.hasOwnProperty.call(msg, 'inline_message');
1884
+ const hasInlineField = hasNonEmptyInlineMessage(msg.inline_message);
1779
1885
  if (client._v2Session && groupId && (legacyPushSeq > 0 || hasInlineField)) {
1780
1886
  await this.processGroupV2MessageCreated(msg, 'v1', inlineGeneration);
1781
1887
  return;
1782
1888
  }
1889
+ if (groupId && legacyPushSeq > 0) {
1890
+ const ns = `group:${groupId}`;
1891
+ client._seqTracker.updateMaxSeen(ns, legacyPushSeq);
1892
+ if (client._sessionOptions?.background_sync !== false)
1893
+ client._safeAsync(this.fillGroupGap(groupId));
1894
+ return;
1895
+ }
1783
1896
  if (payload === undefined || payload === null
1784
1897
  || (typeof payload === 'object' && Object.keys(payload).length === 0)) {
1785
- await this.autoPullGroupMessages(msg);
1898
+ if (client._v2Session)
1899
+ await this.autoPullGroupMessages(msg);
1900
+ else if (groupId && seq !== undefined && seq !== null) {
1901
+ const ns = `group:${groupId}`;
1902
+ const seqNum = positiveSafeSequenceHint(seq);
1903
+ if (seqNum > 0) {
1904
+ client._seqTracker.updateMaxSeen(ns, seqNum);
1905
+ if (seqNum > client._seqTracker.getContiguousSeq(ns)
1906
+ && client._sessionOptions?.background_sync !== false)
1907
+ client._safeAsync(this.fillGroupGap(groupId));
1908
+ }
1909
+ await this.publishOrderedMessage('group.message_created', ns, seq, msg);
1910
+ }
1911
+ else
1912
+ await client._publishAppEvent('group.message_created', msg, 'push');
1786
1913
  return;
1787
1914
  }
1788
1915
  const encryptedPush = client._isEncryptedPushMessage(msg);
1789
- if (groupId && seq !== undefined && seq !== null) {
1790
- const ns = `group:${groupId}`;
1791
- if (seq > 0)
1792
- client._seqTracker.updateMaxSeen(ns, seq);
1793
- const contigBefore = client._seqTracker.getContiguousSeq(ns);
1794
- const seqNeedsPull = client._seqTracker.onMessageSeq(ns, seq);
1795
- // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled,仍占 seq。
1796
- if (!encryptedPush && this.recallEventFromGroupMessage(msg)) {
1797
- await this.publishGroupRecallTombstone(groupId, seq, msg);
1798
- this.markPublishedSeq(ns, Number(seq));
1799
- const contigAfter = client._seqTracker.getContiguousSeq(ns);
1800
- const contig = client._seqTracker.getContiguousSeq(ns);
1801
- if (contig > 0) {
1802
- const ackSeq = this.clampAckSeq('group.ack_messages', 'msg_seq', ns, contig);
1803
- client._transport.call('group.ack_messages', {
1804
- group_id: groupId,
1805
- msg_seq: ackSeq,
1806
- device_id: client._deviceId,
1807
- slot_id: client._slotId,
1808
- _rpc_background: true,
1809
- }).catch((e) => { client._clientLog.warn('group recall auto-ack failed: group=' + groupId, e); });
1810
- }
1811
- if (contigAfter !== contigBefore)
1812
- this.persistSeq(ns);
1813
- return;
1814
- }
1815
- const published = encryptedPush
1816
- ? await client._publishEncryptedPushMessage('group.message_created', 'group.message_undecryptable', ns, seq, msg, true)
1817
- : await this.publishOrderedMessage('group.message_created', ns, seq, msg);
1818
- const contigAfter = client._seqTracker.getContiguousSeq(ns);
1819
- const needPull = seqNeedsPull && !published;
1820
- if (needPull && client._sessionOptions?.background_sync !== false) {
1916
+ const ns = groupId ? `group:${groupId}` : '';
1917
+ const seqNum = positiveSafeSequenceHint(seq);
1918
+ if (ns && seqNum > 0) {
1919
+ client._seqTracker.updateMaxSeen(ns, seqNum);
1920
+ if (seqNum > client._seqTracker.getContiguousSeq(ns)
1921
+ && client._sessionOptions?.background_sync !== false)
1821
1922
  client._safeAsync(this.fillGroupGap(groupId));
1822
- }
1823
- const contig = client._seqTracker.getContiguousSeq(ns);
1824
- if (contig > 0) {
1825
- const ackSeq = this.clampAckSeq('group.ack_messages', 'msg_seq', ns, contig);
1826
- client._transport.call('group.ack_messages', {
1827
- group_id: groupId,
1828
- msg_seq: ackSeq,
1829
- device_id: client._deviceId,
1830
- slot_id: client._slotId,
1831
- _rpc_background: true,
1832
- }).catch((e) => { client._clientLog.warn('group message auto-ack failed: group=' + groupId, e); });
1833
- }
1834
- if (contigAfter !== contigBefore)
1835
- this.persistSeq(ns);
1836
- if (encryptedPush)
1837
- return;
1838
1923
  }
1839
- else {
1840
- if (encryptedPush) {
1841
- await client._publishEncryptedPushMessage('group.message_created', 'group.message_undecryptable', '', seq ?? 0, msg, true);
1842
- return;
1843
- }
1844
- await client._publishAppEvent('group.message_created', msg, 'push');
1924
+ if (this.isSelfSentGroupMessage(msg))
1925
+ return;
1926
+ if (!encryptedPush && this.recallEventFromGroupMessage(msg)) {
1927
+ await this.publishGroupRecallTombstone(groupId, seq, msg);
1928
+ return;
1845
1929
  }
1930
+ if (encryptedPush) {
1931
+ await client._publishEncryptedPushMessage('group.message_created', 'group.message_undecryptable', ns, seq ?? 0, msg, true);
1932
+ return;
1933
+ }
1934
+ if (ns && seq !== undefined && seq !== null) {
1935
+ await this.publishOrderedMessage('group.message_created', ns, seq, msg);
1936
+ }
1937
+ else
1938
+ await client._publishAppEvent('group.message_created', msg, 'push');
1846
1939
  }
1847
1940
  catch (exc) {
1848
1941
  client._clientLog.warn(`group push processing failed:${String(exc)}`);
@@ -1857,7 +1950,7 @@ export class MessageDeliveryEngine {
1857
1950
  _decrypt_error: String(exc),
1858
1951
  };
1859
1952
  client._attachV2EnvelopeMetadataFromSource(safeEvent, data);
1860
- await client._publishAppEvent('group.message_undecryptable', safeEvent);
1953
+ await client._publishAppEvent('group.message_undecryptable', safeEvent, 'push');
1861
1954
  }
1862
1955
  }
1863
1956
  }
@@ -1865,7 +1958,7 @@ export class MessageDeliveryEngine {
1865
1958
  const client = this.runtime.client;
1866
1959
  const groupId = (notification.group_id ?? '');
1867
1960
  if (!groupId) {
1868
- await client._publishAppEvent('group.message_created', notification);
1961
+ await client._publishAppEvent('group.message_created', notification, 'push');
1869
1962
  return;
1870
1963
  }
1871
1964
  if (client._sessionOptions?.background_sync === false) {
@@ -1882,7 +1975,7 @@ export class MessageDeliveryEngine {
1882
1975
  }
1883
1976
  catch (exc) {
1884
1977
  client._clientLog.warn(`auto pull group message failed:${String(exc)}`);
1885
- await client._publishAppEvent('group.message_created', notification);
1978
+ await client._publishAppEvent('group.message_created', notification, 'push');
1886
1979
  return;
1887
1980
  }
1888
1981
  }
@@ -1912,7 +2005,7 @@ export class MessageDeliveryEngine {
1912
2005
  // 两种情况都不能再把原始通知当作第二条业务消息发布。
1913
2006
  client._clientLog.warn(`auto pull group message commit/ack failed:${String(exc)}`);
1914
2007
  if (!rawCompleted) {
1915
- await client._publishAppEvent('group.message_created', notification);
2008
+ await client._publishAppEvent('group.message_created', notification, 'push');
1916
2009
  }
1917
2010
  }
1918
2011
  }
@@ -2050,6 +2143,8 @@ export class MessageDeliveryEngine {
2050
2143
  client._gapFillDone.add(dedupKey);
2051
2144
  this.runtime.delivery.setGapFillActive(true);
2052
2145
  let continuationAfterSeq = 0;
2146
+ let pendingAckSeq = 0;
2147
+ let failed = false;
2053
2148
  try {
2054
2149
  let nextAfterSeq = afterSeq;
2055
2150
  const maxPages = singlePage ? 1 : 100;
@@ -2071,9 +2166,11 @@ export class MessageDeliveryEngine {
2071
2166
  return;
2072
2167
  const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
2073
2168
  const eventObjects = events.filter((evt) => isJsonObject(evt));
2074
- if (eventObjects.length > 0) {
2169
+ const contiguousSeq = Number.isSafeInteger(result.contiguous_seq) ? result.contiguous_seq : undefined;
2170
+ if (contiguousSeq === undefined)
2075
2171
  client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq);
2076
- }
2172
+ else
2173
+ client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq, contiguousSeq);
2077
2174
  const cursor = isJsonObject(result.cursor) ? result.cursor : null;
2078
2175
  const retentionFloor = Math.max(0, Number(result.retention_floor_event_seq ?? 0), Number(cursor?.retention_floor_seq ?? 0));
2079
2176
  const serverAckFloor = Math.max(0, Number(cursor?.current_seq ?? 0));
@@ -2089,6 +2186,7 @@ export class MessageDeliveryEngine {
2089
2186
  }
2090
2187
  const eventSeqs = [];
2091
2188
  let hasDissolvedEvent = false;
2189
+ let publishedEventCount = 0;
2092
2190
  for (const evt of eventObjects) {
2093
2191
  const eventSeq = Number(evt.event_seq ?? 0);
2094
2192
  if (Number.isFinite(eventSeq) && eventSeq > 0)
@@ -2110,7 +2208,8 @@ export class MessageDeliveryEngine {
2110
2208
  }
2111
2209
  }
2112
2210
  if (Number.isFinite(eventSeq) && eventSeq > 0 && !client._pushedSeqs.get(ns)?.has(eventSeq)) {
2113
- this.enqueueOrderedMessage(ns, 'group.changed', eventSeq, evt);
2211
+ this.enqueueOrderedMessage(ns, 'group.changed', eventSeq, evt, 'pull');
2212
+ publishedEventCount += 1;
2114
2213
  }
2115
2214
  }
2116
2215
  const ackContig = client._seqTracker.getContiguousSeq(ns);
@@ -2119,41 +2218,45 @@ export class MessageDeliveryEngine {
2119
2218
  this.persistSeq(ns);
2120
2219
  }
2121
2220
  if (eventObjects.length > 0 && ackContig > 0 && ackContig !== pageContigBefore) {
2122
- try {
2123
- const ackPromise = client._transport.call('group.ack_events', {
2124
- group_id: groupId,
2125
- event_seq: ackContig,
2126
- device_id: client._deviceId,
2127
- slot_id: client._slotId,
2128
- _rpc_background: true,
2129
- });
2130
- if (singlePage)
2131
- await ackPromise;
2132
- else
2133
- ackPromise.catch((e) => { client._clientLog.warn('group event auto-ack failed: group=' + groupId, e); });
2134
- }
2135
- catch (e) {
2136
- client._clientLog.warn('group event auto-ack failed: group=' + groupId, e);
2137
- }
2221
+ pendingAckSeq = Math.max(pendingAckSeq, ackContig);
2138
2222
  }
2223
+ this.onPullPage(ns, publishedEventCount, result.has_more === true, eventObjects.length > 0 ? Number(eventObjects[eventObjects.length - 1].event_seq ?? 0) : undefined, typeof result.remaining === 'number' && Number.isSafeInteger(result.remaining) && result.remaining >= 0 ? result.remaining : null, eventObjects.length, ackContig, eventObjects.length > 0 ? Number(eventObjects[eventObjects.length - 1].event_seq ?? 0) : undefined);
2139
2224
  const nextAfter = Math.max(eventSeqs.length > 0 ? Math.max(...eventSeqs) : nextAfterSeq, nextAfterSeq);
2140
- if (singlePage && result.has_more === true && nextAfter > nextAfterSeq) {
2225
+ const needsMore = result.has_more === true || client._seqTracker.getMaxSeenSeq(ns) > nextAfter;
2226
+ if (singlePage && needsMore && nextAfter > nextAfterSeq) {
2141
2227
  continuationAfterSeq = nextAfter;
2142
2228
  }
2143
- if (eventObjects.length === 0 || nextAfter <= nextAfterSeq || result.has_more !== true)
2229
+ if (eventObjects.length === 0 || nextAfter <= nextAfterSeq || !needsMore)
2144
2230
  break;
2145
2231
  nextAfterSeq = nextAfter;
2146
2232
  }
2147
2233
  if (pageCount >= maxPages) {
2148
2234
  client._clientLog.warn(`group event gap fill reached max_pages=${maxPages} group=${groupId} after_seq=${nextAfterSeq}`);
2149
2235
  }
2236
+ if (pendingAckSeq > 0) {
2237
+ try {
2238
+ await client._transport.call('group.ack_events', {
2239
+ group_id: groupId,
2240
+ event_seq: pendingAckSeq,
2241
+ device_id: client._deviceId,
2242
+ slot_id: client._slotId,
2243
+ _rpc_background: true,
2244
+ });
2245
+ }
2246
+ catch (e) {
2247
+ client._clientLog.warn('group event auto-ack failed: group=' + groupId, e);
2248
+ }
2249
+ }
2150
2250
  }
2151
2251
  catch (exc) {
2252
+ failed = true;
2152
2253
  client._clientLog.warn(`group event gap-fill failed:${String(exc)}`);
2153
2254
  }
2154
2255
  finally {
2155
2256
  client._gapFillDone.delete(dedupKey);
2156
2257
  this.runtime.delivery.setGapFillActive(false);
2258
+ if (!client._rpcPipeline)
2259
+ this.syncStopped(failed ? 'aborted' : 'pull_drained');
2157
2260
  if (singlePage && continuationAfterSeq > afterSeq) {
2158
2261
  this.enqueueOnlineUnreadEventHint({
2159
2262
  group_id: groupId,
@@ -2182,43 +2285,15 @@ export class MessageDeliveryEngine {
2182
2285
  this.enqueueOnlineUnreadEventHint(data);
2183
2286
  return;
2184
2287
  }
2185
- if (this.isSelfJoinGroupChanged(data)) {
2186
- const contig = client._seqTracker.getContiguousSeq(ns);
2187
- const maxSeen = client._seqTracker.getMaxSeenSeq(ns);
2188
- if (contig === 0 && maxSeen === 0 && eventSeq > 1) {
2189
- client._clientLog.debug(`group.changed self-join baseline: group=${groupId}, event_seq=${eventSeq}, baseline=${eventSeq - 1}`);
2190
- client._seqTracker.forceContiguousSeq(ns, eventSeq - 1);
2191
- }
2192
- }
2193
2288
  const contigBefore = client._seqTracker.getContiguousSeq(ns);
2289
+ client._seqTracker.updateMaxSeen(ns, eventSeq);
2194
2290
  const publishedDuplicate = client._pushedSeqs.get(ns)?.has(eventSeq) === true;
2195
2291
  if (eventSeq <= contigBefore || publishedDuplicate) {
2196
2292
  client._clientLog.debug(`group.changed skipped duplicate/stale: group=${groupId}, event_seq=${eventSeq}, contiguous=${contigBefore}`);
2197
- if (eventSeq <= contigBefore) {
2198
- this.fireGroupEventAck(groupId, ns, eventSeq, 'group event covered push ack');
2199
- }
2200
- else if (contigBefore > 0) {
2201
- this.fireGroupEventAck(groupId, ns, contigBefore, 'group event covered push ack');
2202
- }
2203
2293
  return;
2204
2294
  }
2205
2295
  this.enqueueOrderedMessage(ns, 'group.changed', eventSeq, data);
2206
- needPull = client._seqTracker.onMessageSeq(ns, eventSeq);
2207
- const ackContig = client._seqTracker.getContiguousSeq(ns);
2208
- await this.drainOrderedMessages(ns);
2209
- if (ackContig > 0 && ackContig !== contigBefore) {
2210
- if (data.action !== 'dissolved')
2211
- this.persistSeq(ns);
2212
- client._transport.call('group.ack_events', {
2213
- group_id: groupId,
2214
- event_seq: ackContig,
2215
- device_id: client._deviceId,
2216
- slot_id: client._slotId,
2217
- _rpc_background: true,
2218
- }).catch((e) => {
2219
- client._clientLog.warn('group event push auto-ack failed: group=' + groupId, e);
2220
- });
2221
- }
2296
+ needPull = eventSeq > contigBefore;
2222
2297
  if (needPull && groupId && !data._from_gap_fill && client._sessionOptions?.background_sync !== false) {
2223
2298
  client._safeAsync(this.fillGroupEventGap(groupId));
2224
2299
  }
@@ -2837,20 +2912,19 @@ export class MessageDeliveryEngine {
2837
2912
  if (forceTail || order[0] === 'tail')
2838
2913
  result = await run(true, false, undefined, false, forceTail);
2839
2914
  const state = this.syncState(ns);
2840
- // Tail 的连续性证明只覆盖其返回的 head。Tail 进行期间到达的新
2841
- // Push 不能再次进入 realtime planner(那会再做一次 Tail);以已经
2842
- // 落盘的 ack 为起点,至多补一页普通 Forward 即可恢复该小窗口。
2843
- const tailMissedDuringPull = tailCompleted
2844
- && Number(client._seqTracker.getMaxSeenSeq(ns) || 0) > state.head;
2845
2915
  const maxSeen = Number(client._seqTracker.getMaxSeenSeq(ns) || 0);
2916
+ // Tail 期间到达的新 Push 只记录在 pending Pull 上界,不写入 SeqTracker。
2917
+ const pendingUpper = Number(this.pendingP2pPullUpper?.get(ns) ?? 0);
2918
+ const observedUpper = Math.max(maxSeen, pendingUpper);
2919
+ const tailMissedDuringPull = tailCompleted && observedUpper > state.head;
2846
2920
  const pendingThroughSeq = tailMissedDuringPull
2847
- ? maxSeen
2921
+ ? observedUpper
2848
2922
  : 0;
2849
- const tailForward = tailCompleted && (state.ack < state.tail - 1 || tailMissedDuringPull);
2923
+ const tailForward = tailCompleted && (state.ack < state.head || tailMissedDuringPull);
2850
2924
  const backgroundWindowForward = order[0] === 'forward' && !headForward;
2851
2925
  if (headForward || tailForward
2852
2926
  || (client._sessionOptions?.background_sync !== false && backgroundWindowForward)) {
2853
- const forwardTarget = Math.max(headForward ? Math.max(pushSeq, maxSeen) : 0, (tailForward || backgroundWindowForward) ? state.tail - 1 : 0, tailMissedDuringPull ? maxSeen : 0);
2927
+ const forwardTarget = Math.max(headForward ? Math.max(pushSeq, maxSeen) : 0, (tailForward || backgroundWindowForward) ? state.tail - 1 : 0, tailMissedDuringPull ? observedUpper : 0);
2854
2928
  const forwardMaxPages = this.forwardMaxPages(state.ack, forwardTarget, pageLimit);
2855
2929
  result = await run(false, !(headForward || tailForward), forwardMaxPages, headForward || tailMissedDuringPull);
2856
2930
  if (pendingThroughSeq > 0) {
@@ -2912,17 +2986,18 @@ export class MessageDeliveryEngine {
2912
2986
  const state = this.syncState(ns);
2913
2987
  // 与 P2P 相同:真实完成 Tail 后,至多用一页 Forward 收敛 Tail
2914
2988
  // 期间新到达的序号;绝不重新规划或再执行 Tail。
2915
- const tailMissedDuringPull = tailCompleted
2916
- && Number(client._seqTracker.getMaxSeenSeq(ns) || 0) > state.head;
2917
2989
  const maxSeen = Number(client._seqTracker.getMaxSeenSeq(ns) || 0);
2990
+ const pendingUpper = Number(this.pendingGroupPullUpper?.get(ns) ?? 0);
2991
+ const observedUpper = Math.max(maxSeen, pendingUpper);
2992
+ const tailMissedDuringPull = tailCompleted && observedUpper > state.head;
2918
2993
  const pendingThroughSeq = tailMissedDuringPull
2919
- ? maxSeen
2994
+ ? observedUpper
2920
2995
  : 0;
2921
- const tailForward = tailCompleted && (state.ack < state.tail - 1 || tailMissedDuringPull);
2996
+ const tailForward = tailCompleted && (state.ack < state.head || tailMissedDuringPull);
2922
2997
  const backgroundWindowForward = order[0] === 'forward' && !headForward;
2923
2998
  if (headForward || tailForward
2924
2999
  || (client._sessionOptions?.background_sync !== false && backgroundWindowForward)) {
2925
- const forwardTarget = Math.max(headForward ? Math.max(pushSeq, maxSeen) : 0, (tailForward || backgroundWindowForward) ? state.tail - 1 : 0, tailMissedDuringPull ? maxSeen : 0);
3000
+ const forwardTarget = Math.max(headForward ? Math.max(pushSeq, maxSeen) : 0, (tailForward || backgroundWindowForward) ? state.tail - 1 : 0, tailMissedDuringPull ? observedUpper : 0);
2926
3001
  const forwardMaxPages = this.forwardMaxPages(state.ack, forwardTarget, pageLimit);
2927
3002
  result = await run(false, !(headForward || tailForward), forwardMaxPages, headForward || tailMissedDuringPull);
2928
3003
  if (pendingThroughSeq > 0) {
@@ -2979,15 +3054,13 @@ export class MessageDeliveryEngine {
2979
3054
  const groupId = typeof row.group_id === 'string' ? row.group_id.trim() : '';
2980
3055
  if (!groupId)
2981
3056
  return;
2982
- const seq = typeof row.seq === 'number' ? row.seq : 0;
2983
3057
  const eventKind = String(row.kind ?? '').trim();
2984
3058
  const ns = this.resolvePendingGroupInlineAckNamespace(`group:${groupId}`);
2985
- if (client._v2Session && eventKind !== 'group.online_unread_hint'
2986
- && Number.isSafeInteger(seq) && seq > 0) {
2987
- // 必须在进入 namespace 队列前登记,才能让在途 Tail 看见后来 Push。
3059
+ const seq = positiveSafeSequenceHint(row.seq);
3060
+ if (client._v2Session && eventKind !== 'group.online_unread_hint' && seq > 0) {
2988
3061
  client._seqTracker.updateMaxSeen(ns, seq);
2989
3062
  }
2990
- const hasInlineObject = isJsonObject(row.inline_message)
3063
+ const hasInlineObject = hasNonEmptyInlineMessage(row.inline_message)
2991
3064
  && eventKind !== 'group.online_unread_hint';
2992
3065
  const inlineGeneration = hasInlineObject ? this.captureInlineGeneration() : undefined;
2993
3066
  const operation = () => this.processGroupV2MessageCreated(data, 'v2', inlineGeneration);
@@ -3004,7 +3077,6 @@ export class MessageDeliveryEngine {
3004
3077
  return;
3005
3078
  const groupId = typeof data.group_id === 'string' ? data.group_id.trim() : '';
3006
3079
  const seq = positiveSafeSequenceHint(data.seq);
3007
- const strictSeq = typeof data.seq === 'number' && Number.isSafeInteger(data.seq) && data.seq > 0;
3008
3080
  if (!groupId)
3009
3081
  return;
3010
3082
  const eventKind = String(data.kind ?? '').trim();
@@ -3016,12 +3088,13 @@ export class MessageDeliveryEngine {
3016
3088
  return;
3017
3089
  }
3018
3090
  const ns = this.resolvePendingGroupInlineAckNamespace(`group:${groupId}`);
3019
- const inlinePresent = Object.prototype.hasOwnProperty.call(data, 'inline_message');
3091
+ const inlinePresent = hasNonEmptyInlineMessage(data.inline_message);
3020
3092
  if (seq <= 0) {
3021
3093
  if (inlinePresent)
3022
3094
  await this.recoverInvalidGroupRealtimeHead(groupId, ns, data.seq);
3023
3095
  return;
3024
3096
  }
3097
+ client._seqTracker.updateMaxSeen(ns, seq);
3025
3098
  const inline = this.inlineMessage(data);
3026
3099
  const inlineMatches = inline !== null
3027
3100
  && this.inlineGroupBindingMatches(data, inline, groupId, seq, expectedInlineVersion);
@@ -3040,15 +3113,13 @@ export class MessageDeliveryEngine {
3040
3113
  const before = this.syncState(ns);
3041
3114
  // 无正文的旧服务端 Head 可把 JSON-safe 数字字符串作为恢复 hint;
3042
3115
  // inline 绑定仍要求原始 seq 是严格 number,避免类型归一化后消费正文。
3043
- if (strictSeq || !inlinePresent)
3044
- client._seqTracker.updateMaxSeen(ns, seq);
3045
3116
  if (seq <= before.ack && before.ack >= before.tail - 1) {
3046
3117
  const canonicalNs = this.resolvePendingGroupInlineAckNamespace(ns);
3047
3118
  const pending = this.pendingGroupInlineAcks?.get(canonicalNs);
3048
- if (pending && seq >= pending.seq) {
3119
+ if (inlinePresent && pending && seq >= pending.seq) {
3049
3120
  this.retryPendingGroupInlineAck(canonicalNs, pending.seq, 'covered inline Group ack', seq);
3050
3121
  }
3051
- else if (!inlinePresent || inlineMatches) {
3122
+ else if (inlineMatches) {
3052
3123
  this.fireGroupV2Ack(groupId, ns, seq, 'group v2 covered push ack', inlineMatches ? inlineGeneration : null);
3053
3124
  }
3054
3125
  return;
@@ -3084,11 +3155,10 @@ export class MessageDeliveryEngine {
3084
3155
  }
3085
3156
  const pushSeq = isJsonObject(data) && typeof data.seq === 'number' ? data.seq : 0;
3086
3157
  if (client._v2Session && Number.isSafeInteger(pushSeq) && pushSeq > 0) {
3087
- // 与 Group 相同,在排队前暴露更高 Head 给当前在途 Tail。
3088
3158
  client._seqTracker.updateMaxSeen(ns, pushSeq);
3089
3159
  }
3090
3160
  const hasInlineObject = isJsonObject(data)
3091
- && isJsonObject(data.inline_message);
3161
+ && hasNonEmptyInlineMessage(data.inline_message);
3092
3162
  const inlineGeneration = hasInlineObject ? this.captureInlineGeneration() : undefined;
3093
3163
  const operation = () => this.processV2P2PNotification(data, 'v2', inlineGeneration);
3094
3164
  if (hasInlineObject)
@@ -3103,12 +3173,12 @@ export class MessageDeliveryEngine {
3103
3173
  if (!client._v2Session)
3104
3174
  return;
3105
3175
  const pushSeq = isJsonObject(data) ? positiveSafeSequenceHint(data.seq) : 0;
3106
- const strictSeq = isJsonObject(data)
3107
- && typeof data.seq === 'number' && Number.isSafeInteger(data.seq) && data.seq > 0;
3108
3176
  const ns = client._aid ? `p2p:${client._aid}` : '';
3109
3177
  if (!ns)
3110
3178
  return;
3111
- const inlinePresent = isJsonObject(data) && Object.prototype.hasOwnProperty.call(data, 'inline_message');
3179
+ if (pushSeq > 0)
3180
+ client._seqTracker.updateMaxSeen(ns, pushSeq);
3181
+ const inlinePresent = isJsonObject(data) && hasNonEmptyInlineMessage(data.inline_message);
3112
3182
  if (pushSeq <= 0) {
3113
3183
  if (inlinePresent)
3114
3184
  await this.recoverInvalidP2PRealtimeHead(ns, isJsonObject(data) ? data.seq : undefined);
@@ -3128,11 +3198,9 @@ export class MessageDeliveryEngine {
3128
3198
  if (inlineMatches && !this.isInlineGenerationCurrent(inlineGeneration))
3129
3199
  return;
3130
3200
  const before = this.syncState(ns);
3131
- if (strictSeq || !inlinePresent)
3132
- client._seqTracker.updateMaxSeen(ns, pushSeq);
3133
3201
  if (pushSeq <= before.ack && before.ack >= before.tail - 1) {
3134
3202
  const pending = this.pendingP2PInlineAcks?.get(ns);
3135
- if (pending && pushSeq >= pending.seq) {
3203
+ if (inlinePresent && pending && pushSeq >= pending.seq) {
3136
3204
  this.retryPendingP2PInlineAck(ns, pending.seq, 'covered inline P2P ack', pushSeq);
3137
3205
  }
3138
3206
  else if (inlineMatches) {
@@ -3651,7 +3719,7 @@ export class MessageDeliveryEngine {
3651
3719
  const seqNum = Number(seq);
3652
3720
  if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0 || !ns) {
3653
3721
  if (event === 'message.recalled') {
3654
- const published = await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seq, payload));
3722
+ const published = await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seq, payload, source));
3655
3723
  this.ensurePullOperationCurrent();
3656
3724
  return published;
3657
3725
  }
@@ -3672,7 +3740,7 @@ export class MessageDeliveryEngine {
3672
3740
  if (queue && queue.size === 0)
3673
3741
  client._pendingOrderedMsgs.delete(ns);
3674
3742
  if (event === 'message.recalled') {
3675
- const published = await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seqNum, payload));
3743
+ const published = await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seqNum, payload, source));
3676
3744
  this.ensurePullOperationCurrent();
3677
3745
  this.markPublishedSeq(ns, seqNum);
3678
3746
  return published;