@agentunion/fastaun 0.5.11 → 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
  }
@@ -203,14 +207,18 @@ export class MessageDeliveryEngine {
203
207
  publishedCount += 1;
204
208
  }
205
209
  }
210
+ else if (this.isSelfSentGroupMessage(message)) {
211
+ this.markPublishedSeq(ns, seq);
212
+ }
206
213
  else if (await this.publishPulledMessage('group.message_created', ns, seq, message, false)) {
207
214
  this.ensurePullOperationCurrent();
208
215
  publishedCount += 1;
209
216
  }
210
217
  }
211
218
  this.ensurePullOperationCurrent();
212
- if (messages.length > 0)
213
- client._seqTracker.onPullResult(ns, messages, afterSeq);
219
+ client._seqTracker.onPullResult(ns, messages, afterSeq, Number.isSafeInteger(result.contiguous_seq)
220
+ ? result.contiguous_seq
221
+ : undefined);
214
222
  const commitTarget = Math.max(client._seqTracker.getContiguousSeq(ns), retentionFloor, visibilityFloor, deferredServerCursor);
215
223
  if (commitTarget > client._seqTracker.getContiguousSeq(ns)) {
216
224
  client._seqTracker.forceContiguousSeq(ns, commitTarget);
@@ -554,6 +562,9 @@ export class MessageDeliveryEngine {
554
562
  hasPendingPull(ns) {
555
563
  return (this.pendingGroupPullUpper?.get(ns) ?? 0) > 0;
556
564
  }
565
+ pendingPullUpper(ns) {
566
+ return this.pendingGroupPullUpper?.get(ns) ?? 0;
567
+ }
557
568
  hasAnyPendingPull() {
558
569
  return [...(this.pendingGroupPullUpper?.values() ?? [])].some((seq) => seq > 0)
559
570
  || Boolean(this.onlineUnreadHintTasks?.size)
@@ -1160,35 +1171,11 @@ export class MessageDeliveryEngine {
1160
1171
  return;
1161
1172
  }
1162
1173
  const ns = `group:${groupId}`;
1163
- if (seqNum > 0) {
1174
+ if (seqNum > 0)
1164
1175
  client._seqTracker.updateMaxSeen(ns, seqNum);
1165
- if (client._seqTracker.getContiguousSeq(ns) === seqNum) {
1166
- // 已覆盖(pull 先到并推进过),仍走去重发布兜底,不重复推进 seq。
1167
- await this.publishGroupRecallTombstone(groupId, seq, wrapped);
1168
- return;
1169
- }
1170
- client._repairPushContiguousBound(ns, seqNum, true, '_raw.group.message_recalled');
1171
- }
1172
- // 该 notice_seq 已由 pull 路径处理过(已发布或挂起待发布)时,去重发布兜底后返回。
1173
- const pushed = client._pushedSeqs.get(ns);
1174
- const pending = client._pendingOrderedMsgs.get(ns);
1175
- if (pushed?.has(seqNum) || pending?.has(seqNum)) {
1176
- await this.publishGroupRecallTombstone(groupId, seq, wrapped);
1177
- 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)}`));
1178
1178
  }
1179
- const contigBefore = client._seqTracker.getContiguousSeq(ns);
1180
- client._seqTracker.onMessageSeq(ns, seqNum);
1181
- await this.publishGroupRecallTombstone(groupId, seq, wrapped);
1182
- this.markPublishedSeq(ns, seqNum);
1183
- client._markOrderedSeqDelivered?.(ns, seqNum);
1184
- const contig = client._seqTracker.getContiguousSeq(ns);
1185
- if (contig > 0) {
1186
- const ackSeq = this.clampAckSeq('group.ack_messages', 'msg_seq', ns, contig);
1187
- client._withBackgroundRpc(() => client._ackGroupV2(groupId, ackSeq))
1188
- .catch((e) => { client._clientLog.debug(`group recall auto-ack failed: group=${groupId} ${formatDeliveryError(e)}`); });
1189
- }
1190
- if (contig !== contigBefore)
1191
- this.persistSeq(ns);
1192
1179
  }
1193
1180
  publishAppEvent(event, payload, source = 'direct', deliveryContext) {
1194
1181
  const client = this.runtime.client;
@@ -1330,6 +1317,21 @@ export class MessageDeliveryEngine {
1330
1317
  }
1331
1318
  return true;
1332
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
+ }
1333
1335
  async runPushProcessSerialized(ns, operation, inlineGeneration) {
1334
1336
  const client = this.runtime.client;
1335
1337
  if (inlineGeneration === undefined) {
@@ -1397,7 +1399,7 @@ export class MessageDeliveryEngine {
1397
1399
  ? `p2p:${client._aid}`
1398
1400
  : '';
1399
1401
  const inlineGeneration = isJsonObject(data)
1400
- && Object.prototype.hasOwnProperty.call(data, 'inline_message')
1402
+ && hasNonEmptyInlineMessage(data.inline_message)
1401
1403
  ? this.captureInlineAckGeneration() : undefined;
1402
1404
  this.runPushProcessSerialized(ns, () => this.processAndPublishMessage(data, inlineGeneration), inlineGeneration).catch((exc) => {
1403
1405
  client._clientLog.warn(`P2P message decrypt failed: ${formatDeliveryError(exc)}`);
@@ -1432,20 +1434,9 @@ export class MessageDeliveryEngine {
1432
1434
  const seqNum = Number(seq);
1433
1435
  if (Number.isFinite(seqNum) && seqNum > 0)
1434
1436
  client._seqTracker.updateMaxSeen(ns, seqNum);
1435
- const contigBefore = client._seqTracker.getContiguousSeq(ns);
1436
- const published = await this.publishOrderedMessage('message.recalled', ns, seq, msg);
1437
- const contigAfter = client._seqTracker.getContiguousSeq(ns);
1438
- if (Number(seq) > contigAfter && !published && client._sessionOptions?.background_sync !== false) {
1437
+ if (Number(seq) > client._seqTracker.getContiguousSeq(ns) && client._sessionOptions?.background_sync !== false) {
1439
1438
  this.fillP2pGap().catch((exc) => client._clientLog.warn(`background recall gap fill failed: ${formatDeliveryError(exc)}`));
1440
1439
  }
1441
- const contig = client._seqTracker.getContiguousSeq(ns);
1442
- if (contig > 0) {
1443
- const ackSeq = this.clampAckSeq('message.ack', 'seq', ns, contig);
1444
- client._withBackgroundRpc(() => client._ackV2(ackSeq))
1445
- .catch((e) => { client._clientLog.debug(`P2P recall auto-ack failed: ${formatDeliveryError(e)}`); });
1446
- }
1447
- if (contigAfter !== contigBefore)
1448
- this.persistSeq(ns);
1449
1440
  return;
1450
1441
  }
1451
1442
  await this.publishMessageRecallTombstone(seq, msg);
@@ -1464,7 +1455,7 @@ export class MessageDeliveryEngine {
1464
1455
  // 新 SDK 的所有带合法 seq P2P Push 都只作为 Head 通知处理。
1465
1456
  // 新服务端为兼容旧 SDK 仍可能发送 message.received;V2 会话下必须
1466
1457
  // 在旧 seq tracking / ordered publish 之前转入前台 Tail 协调器。
1467
- const inlinePresent = Object.prototype.hasOwnProperty.call(msg, 'inline_message');
1458
+ const inlinePresent = hasNonEmptyInlineMessage(msg.inline_message);
1468
1459
  const legacyPushSeq = this.realtimeHeadSeq(msg.seq, inlinePresent);
1469
1460
  if (client._v2Session && client._aid && (msg.seq !== undefined && msg.seq !== null || inlinePresent)) {
1470
1461
  if (legacyPushSeq === null) {
@@ -1483,29 +1474,11 @@ export class MessageDeliveryEngine {
1483
1474
  const ns = `p2p:${client._aid}`;
1484
1475
  if (seq > 0)
1485
1476
  client._seqTracker.updateMaxSeen(ns, seq);
1486
- const contigBefore = client._seqTracker.getContiguousSeq(ns);
1487
- const published = encryptedPush
1488
- ? await client._publishEncryptedPushMessage('message.received', 'message.undecryptable', ns, seq, msg, false)
1489
- : await this.publishOrderedMessage('message.received', ns, seq, msg);
1490
- const contigAfter = client._seqTracker.getContiguousSeq(ns);
1491
- const needPull = Number(seq) > contigAfter && !published;
1492
- if (needPull && client._sessionOptions?.background_sync !== false) {
1493
- 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)}`);
1494
1479
  this.fillP2pGap().catch((exc) => client._clientLog.warn(`background gap fill trigger failed: ${formatDeliveryError(exc)}`));
1495
1480
  }
1496
- const contig = client._seqTracker.getContiguousSeq(ns);
1497
- if (contig > 0) {
1498
- const maxSeen = client._seqTracker.getMaxSeenSeq(ns);
1499
- const ackSeq = this.clampAckSeq('message.ack', 'seq', ns, contig);
1500
- client._clientLog.debug(`P2P push auto-ack send: ns=${ns}, seq=${ackSeq}, contiguous=${contig}, max_seen=${maxSeen}`);
1501
- client._withBackgroundRpc(() => client._ackV2(ackSeq))
1502
- .then(() => { client._clientLog.debug(`P2P push auto-ack ok: ns=${ns}, seq=${ackSeq}`); })
1503
- .catch((e) => { client._clientLog.debug(`P2P auto-ack failed: ${formatDeliveryError(e)}`); });
1504
- }
1505
- if (contigAfter !== contigBefore)
1506
- this.persistSeq(ns);
1507
- if (encryptedPush)
1508
- return;
1481
+ return;
1509
1482
  }
1510
1483
  else {
1511
1484
  if (encryptedPush) {
@@ -1529,7 +1502,7 @@ export class MessageDeliveryEngine {
1529
1502
  ? String(data.group_id ?? '').trim()
1530
1503
  : '';
1531
1504
  const inlineGeneration = isJsonObject(data)
1532
- && Object.prototype.hasOwnProperty.call(data, 'inline_message')
1505
+ && hasNonEmptyInlineMessage(data.inline_message)
1533
1506
  ? this.captureInlineAckGeneration() : undefined;
1534
1507
  this.runPushProcessSerialized(groupNs ? `group:${groupNs}` : '', () => this.processAndPublishGroupMessage(data, inlineGeneration), inlineGeneration).catch((exc) => {
1535
1508
  client._clientLog.warn(`group message decrypt failed: ${formatDeliveryError(exc)}`);
@@ -1564,7 +1537,7 @@ export class MessageDeliveryEngine {
1564
1537
  }
1565
1538
  // V2 会话中旧 group.message_created 也只是 Head 通知;必须先 Tail,
1566
1539
  // 不能让 payload 路径直接推进旧 ordered publish 状态。
1567
- const inlinePresent = Object.prototype.hasOwnProperty.call(msg, 'inline_message');
1540
+ const inlinePresent = hasNonEmptyInlineMessage(msg.inline_message);
1568
1541
  const legacyPushSeq = this.realtimeHeadSeq(seq, inlinePresent);
1569
1542
  if (client._v2Session && groupId && (seq !== undefined && seq !== null || inlinePresent)) {
1570
1543
  if (legacyPushSeq === null) {
@@ -1589,46 +1562,15 @@ export class MessageDeliveryEngine {
1589
1562
  const ns = `group:${groupId}`;
1590
1563
  if (seq > 0)
1591
1564
  client._seqTracker.updateMaxSeen(ns, seq);
1592
- const contigBefore = client._seqTracker.getContiguousSeq(ns);
1593
- // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled,仍占 seq 推进 contiguous/ack。
1594
- if (!encryptedPush && this.recallEventFromGroupMessage(msg)) {
1595
- const published = await this.publishOrderedGroupRecall(ns, seq, msg);
1596
- const contigAfter = client._seqTracker.getContiguousSeq(ns);
1597
- const contig = client._seqTracker.getContiguousSeq(ns);
1598
- if (contig > 0) {
1599
- const ackSeq = this.clampAckSeq('group.ack_messages', 'msg_seq', ns, contig);
1600
- client._withBackgroundRpc(() => client._ackGroupV2(groupId, ackSeq))
1601
- .catch((e) => { client._clientLog.debug(`group recall auto-ack failed: group=${groupId} ${formatDeliveryError(e)}`); });
1602
- }
1603
- if (contigAfter !== contigBefore)
1604
- this.persistSeq(ns);
1605
- void published;
1606
- return;
1607
- }
1608
- const published = encryptedPush
1609
- ? await client._publishEncryptedPushMessage('group.message_created', 'group.message_undecryptable', ns, seq, msg, true)
1610
- : await this.publishOrderedMessage('group.message_created', ns, seq, msg);
1611
- const contigAfter = client._seqTracker.getContiguousSeq(ns);
1612
- const needPull = Number(seq) > contigAfter && !published;
1613
- if (needPull && client._sessionOptions?.background_sync !== false) {
1614
- 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)}`);
1615
1567
  this.fillGroupGap(groupId).catch((exc) => client._clientLog.warn(`background gap fill trigger failed: ${formatDeliveryError(exc)}`));
1616
1568
  }
1617
- const contig = client._seqTracker.getContiguousSeq(ns);
1618
- if (contig > 0) {
1619
- const maxSeen = client._seqTracker.getMaxSeenSeq(ns);
1620
- const ackSeq = this.clampAckSeq('group.ack_messages', 'msg_seq', ns, contig);
1621
- client._clientLog.debug(`group push auto-ack send: group=${groupId}, ns=${ns}, seq=${ackSeq}, contiguous=${contig}, max_seen=${maxSeen}`);
1622
- client._withBackgroundRpc(() => client._ackGroupV2(groupId, ackSeq))
1623
- .then(() => { client._clientLog.debug(`group push auto-ack ok: group=${groupId}, seq=${ackSeq}`); })
1624
- .catch((e) => { client._clientLog.debug(`group message auto-ack failed: group=${groupId} ${formatDeliveryError(e)}`); });
1625
- }
1626
- if (contigAfter !== contigBefore)
1627
- this.persistSeq(ns);
1628
- if (encryptedPush)
1629
- return;
1569
+ return;
1630
1570
  }
1631
1571
  else {
1572
+ if (this.isSelfSentGroupMessage(msg))
1573
+ return;
1632
1574
  if (encryptedPush) {
1633
1575
  await client._publishEncryptedPushMessage('group.message_created', 'group.message_undecryptable', '', seq ?? 0, msg, true);
1634
1576
  return;
@@ -1824,9 +1766,9 @@ export class MessageDeliveryEngine {
1824
1766
  return;
1825
1767
  const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
1826
1768
  const eventObjects = events.filter((evt) => isJsonObject(evt));
1827
- if (eventObjects.length > 0) {
1828
- client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq);
1829
- }
1769
+ client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq, Number.isSafeInteger(result.contiguous_seq)
1770
+ ? result.contiguous_seq
1771
+ : undefined);
1830
1772
  const cursor = isJsonObject(result.cursor)
1831
1773
  ? result.cursor
1832
1774
  : null;
@@ -1907,10 +1849,13 @@ export class MessageDeliveryEngine {
1907
1849
  targetSeq: Math.max(Number(cursor?.latest_seq ?? 0), Number(result.latest_event_seq ?? 0)),
1908
1850
  });
1909
1851
  const nextAfter = Math.max(eventSeqs.length > 0 ? Math.max(...eventSeqs) : nextAfterSeq, nextAfterSeq);
1910
- 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) {
1911
1856
  continuationAfterSeq = nextAfter;
1912
1857
  }
1913
- if (eventObjects.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
1858
+ if (!pageAdvanced || !needsMore)
1914
1859
  break;
1915
1860
  nextAfterSeq = nextAfter;
1916
1861
  }
@@ -1973,93 +1918,19 @@ export class MessageDeliveryEngine {
1973
1918
  this.enqueueOnlineUnreadEventHint(data);
1974
1919
  return;
1975
1920
  }
1976
- if (this.isSelfJoinGroupChanged(data)) {
1977
- const contig = client._seqTracker.getContiguousSeq(ns);
1978
- const maxSeen = client._seqTracker.getMaxSeenSeq(ns);
1979
- if (contig === 0 && maxSeen === 0 && eventSeq > 1) {
1980
- client._clientLog.debug(`group.changed self-join baseline: group=${groupId}, event_seq=${eventSeq}, baseline=${eventSeq - 1}`);
1981
- client._seqTracker.forceContiguousSeq(ns, eventSeq - 1);
1982
- }
1983
- }
1984
1921
  const contigBefore = client._seqTracker.getContiguousSeq(ns);
1985
1922
  if (eventSeq <= contigBefore || client._pushedSeqs.get(ns)?.has(eventSeq)) {
1986
1923
  client._clientLog.debug(`group.changed skipped duplicate/stale: group=${groupId}, event_seq=${eventSeq}, contiguous=${contigBefore}`);
1987
- this.fireGroupEventAck(groupId, Math.min(eventSeq, contigBefore > 0 ? contigBefore : eventSeq), 'covered push');
1988
1924
  return;
1989
1925
  }
1990
- this.enqueueOrderedMessage(ns, 'group.changed', eventSeq, data);
1991
- needPull = client._seqTracker.onMessageSeq(ns, eventSeq);
1992
- const ackContig = client._seqTracker.getContiguousSeq(ns);
1993
- await this.drainOrderedMessages(ns);
1994
- if (ackContig > 0 && ackContig !== contigBefore) {
1995
- if (data.action !== 'dissolved')
1996
- this.persistSeq(ns);
1997
- client._transport.call('group.ack_events', {
1998
- group_id: groupId,
1999
- event_seq: ackContig,
2000
- device_id: client._deviceId,
2001
- slot_id: client._slotId,
2002
- }, undefined, undefined, true).catch((e) => {
2003
- client._clientLog.debug(`group event push auto-ack failed: group=${groupId} ${formatDeliveryError(e)}`);
2004
- });
2005
- }
2006
- 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) {
2007
1929
  this.fillGroupEventGap(groupId).catch((exc) => {
2008
1930
  client._clientLog.warn(`background gap fill trigger failed: ${formatDeliveryError(exc)}`);
2009
1931
  });
2010
1932
  }
2011
1933
  }
2012
- fireGroupEventAck(groupId, eventSeq, reason) {
2013
- const client = this.runtime.client;
2014
- const gid = String(groupId ?? '').trim();
2015
- if (!gid)
2016
- return;
2017
- const ns = `group_event:${gid}`;
2018
- const ackSeq = this.clampAckSeq('group.ack_events', 'event_seq', ns, Number(eventSeq) || 0);
2019
- if (ackSeq <= 0)
2020
- return;
2021
- const params = {
2022
- group_id: gid,
2023
- event_seq: ackSeq,
2024
- device_id: client._deviceId,
2025
- slot_id: client._slotId,
2026
- _rpc_background: true,
2027
- };
2028
- try {
2029
- Promise.resolve(client.call('group.ack_events', params)).catch((e) => {
2030
- client._clientLog.debug(`group event ${reason} ack failed: group=${gid} ${formatDeliveryError(e)}`);
2031
- });
2032
- }
2033
- catch (e) {
2034
- client._clientLog.debug(`group event ${reason} ack failed: group=${gid} ${formatDeliveryError(e)}`);
2035
- }
2036
- }
2037
- fireGroupV2Ack(groupId, upToSeq, reason, groupAid = '') {
2038
- const client = this.runtime.client;
2039
- const gid = String(groupId ?? '').trim();
2040
- if (!gid)
2041
- return;
2042
- const ns = `group:${gid}`;
2043
- const ackSeq = this.clampAckSeq('group.v2.ack', 'up_to_seq', ns, Number(upToSeq) || 0);
2044
- if (ackSeq <= 0)
2045
- return;
2046
- const generation = this.inlineAckGeneration;
2047
- try {
2048
- Promise.resolve(client.call('group.v2.ack', {
2049
- group_id: gid,
2050
- ...(groupAid ? { group_aid: groupAid } : {}),
2051
- up_to_seq: ackSeq,
2052
- _rpc_background: true,
2053
- })).then((result) => {
2054
- this.onGroupV2AckResult(gid, result, ackSeq, generation);
2055
- }).catch((e) => {
2056
- client._clientLog.debug(`group.v2 ${reason} ack failed: group=${gid} ${formatDeliveryError(e)}`);
2057
- });
2058
- }
2059
- catch (e) {
2060
- client._clientLog.debug(`group.v2 ${reason} ack failed: group=${gid} ${formatDeliveryError(e)}`);
2061
- }
2062
- }
2063
1934
  isSelfJoinGroupChanged(data) {
2064
1935
  const action = String(data.action ?? '').trim();
2065
1936
  if (!['member_added', 'joined', 'join_approved', 'invite_code_used'].includes(action))
@@ -2356,15 +2227,17 @@ export class MessageDeliveryEngine {
2356
2227
  const state = this.syncState(ns);
2357
2228
  const needsForward = state.ack < state.tail - 1;
2358
2229
  const backgroundEnabled = client._sessionOptions?.background_sync !== false;
2359
- // Tail 期间到达的新 Push 可能只更新 maxSeen 而没有扩展 Tail 窗口。
2230
+ // Tail 期间到达的新 Push 只记录在 pending Pull 上界,不写入 SeqTracker。
2360
2231
  const maxSeen = Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? 0);
2361
- 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;
2362
2235
  const tailForward = tailHead !== null && (needsForward || tailMissedDuringPull);
2363
2236
  const backgroundWindowForward = (order.length === 1 && order[0] === 'forward' && !headForward)
2364
2237
  || (order.length === 1 && order[0] === 'tail' && tailHead === null && needsForward);
2365
2238
  const foregroundForward = headForward || tailForward;
2366
2239
  if (foregroundForward || backgroundEnabled && backgroundWindowForward) {
2367
- 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);
2368
2241
  const requiredPages = Math.max(1, Math.ceil(Math.max(0, forwardTarget - state.ack) / pageLimit));
2369
2242
  result = await run({
2370
2243
  after_seq: state.ack,
@@ -2432,13 +2305,15 @@ export class MessageDeliveryEngine {
2432
2305
  const needsForward = state.ack < state.tail - 1;
2433
2306
  const backgroundEnabled = client._sessionOptions?.background_sync !== false;
2434
2307
  const maxSeen = Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? 0);
2435
- 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;
2436
2311
  const tailForward = tailHead !== null && (needsForward || tailMissedDuringPull);
2437
2312
  const backgroundWindowForward = (order.length === 1 && order[0] === 'forward' && !headForward)
2438
2313
  || (order.length === 1 && order[0] === 'tail' && tailHead === null && needsForward);
2439
2314
  const foregroundForward = headForward || tailForward;
2440
2315
  if (foregroundForward || backgroundEnabled && backgroundWindowForward) {
2441
- 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);
2442
2317
  const requiredPages = Math.max(1, Math.ceil(Math.max(0, forwardTarget - state.ack) / pageLimit));
2443
2318
  result = await run({
2444
2319
  after_seq: state.ack,
@@ -3048,7 +2923,7 @@ export class MessageDeliveryEngine {
3048
2923
  this.enqueueOnlineUnreadP2pHint(pushData);
3049
2924
  return;
3050
2925
  }
3051
- const inlinePresent = Object.prototype.hasOwnProperty.call(pushData, 'inline_message');
2926
+ const inlinePresent = hasNonEmptyInlineMessage(pushData.inline_message);
3052
2927
  const pushSeq = this.realtimeHeadSeq(rawPushSeq, inlinePresent);
3053
2928
  if (pushSeq === null) {
3054
2929
  if (inlinePresent) {
@@ -3079,7 +2954,8 @@ export class MessageDeliveryEngine {
3079
2954
  client._seqTracker.updateMaxSeen(ns, pushSeq);
3080
2955
  if (pushSeq <= before.ack && before.ack >= before.tail - 1) {
3081
2956
  client._clientLog.debug(`V2 P2P duplicate push ignored: seq=${pushSeq} A/T/H=${JSON.stringify(before)}`);
3082
- this.retryPendingP2PInlineAck(ns, pushSeq, 'covered inline push');
2957
+ if (inlinePresent)
2958
+ this.retryPendingP2PInlineAck(ns, pushSeq, 'covered inline push');
3083
2959
  return;
3084
2960
  }
3085
2961
  const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
@@ -3152,7 +3028,7 @@ export class MessageDeliveryEngine {
3152
3028
  client._logMessageDebug('server-push', '_raw.group.v2.message_created', 'group.message_created', d);
3153
3029
  const groupId = typeof d.group_id === 'string' ? d.group_id.trim() : '';
3154
3030
  const rawSeq = d.seq;
3155
- const inlinePresent = Object.prototype.hasOwnProperty.call(d, 'inline_message');
3031
+ const inlinePresent = hasNonEmptyInlineMessage(d.inline_message);
3156
3032
  const seq = this.realtimeHeadSeq(rawSeq, inlinePresent);
3157
3033
  if (!groupId) {
3158
3034
  client._clientLog.debug(`_onRawGroupV2MessageCreated skipped: group=${groupId || '<empty>'}, seq=${String(d.seq ?? '')}`);
@@ -3197,12 +3073,9 @@ export class MessageDeliveryEngine {
3197
3073
  if (seq <= before.ack && before.ack >= before.tail - 1) {
3198
3074
  client._clientLog.debug(`V2 group duplicate push ignored: group=${groupId} seq=${seq} A/T/H=${JSON.stringify(before)}`);
3199
3075
  const pending = this.pendingGroupInlineAcks?.get(ns);
3200
- if (pending?.seq === seq) {
3076
+ if (inlinePresent && pending?.seq === seq) {
3201
3077
  this.retryPendingGroupInlineAck(ns, seq, 'covered inline push');
3202
3078
  }
3203
- else if (!inlinePresent) {
3204
- this.fireGroupV2Ack(groupId, seq, 'covered push');
3205
- }
3206
3079
  return;
3207
3080
  }
3208
3081
  const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
@@ -3752,32 +3625,6 @@ export class MessageDeliveryEngine {
3752
3625
  }
3753
3626
  return true;
3754
3627
  }
3755
- async publishOrderedGroupRecall(ns, seq, message) {
3756
- const client = this.runtime.client;
3757
- this.ensurePullOperationCurrent();
3758
- const seqNum = Number(seq);
3759
- if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0) {
3760
- await this.publishGroupRecallTombstone(ns.replace(/^group:/, ''), seq, message);
3761
- this.ensurePullOperationCurrent();
3762
- return true;
3763
- }
3764
- // 撤回 tombstone 也占 seq:推进 contiguous 并标记已发布,确保 ack 正常推进、不留空洞。
3765
- if (client._pushedSeqs.get(ns)?.has(seqNum)) {
3766
- await this.publishGroupRecallTombstone(ns.replace(/^group:/, ''), seq, message);
3767
- return false;
3768
- }
3769
- client._seqTracker.onMessageSeq(ns, seqNum);
3770
- await this.publishGroupRecallTombstone(ns.replace(/^group:/, ''), seq, message);
3771
- this.ensurePullOperationCurrent();
3772
- this.markPublishedSeq(ns, seqNum);
3773
- client._markOrderedSeqDelivered?.(ns, seqNum);
3774
- await this.drainOrderedMessages(ns);
3775
- if (!client._pendingOrderedMsgs.get(ns)) {
3776
- this.ensurePullOperationCurrent();
3777
- await this.saveSeqTrackerState();
3778
- }
3779
- return true;
3780
- }
3781
3628
  async publishPulledMessage(event, ns, seq, payload, persist = true, source = 'pull') {
3782
3629
  const client = this.runtime.client;
3783
3630
  this.ensurePullOperationCurrent();