@agentunion/fastaun-browser 0.5.6 → 0.5.8

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 (102) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/_packed_docs/CHANGELOG.md +74 -0
  3. package/_packed_docs/INDEX.md +12 -11
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +4 -3
  5. package/_packed_docs/agent.md/SCHEMA.md +83 -58
  6. package/_packed_docs/agent.md/examples/codeagent-claudecode.md +1 -1
  7. package/_packed_docs/agent.md/examples/openclaw-lobster.md +1 -1
  8. package/_packed_docs/agent.md/examples/signed-openclaw-lobster.md +1 -1
  9. package/_packed_docs/audit/AUN/346/234/215/345/212/241Go/345/214/226/351/207/215/346/236/204/347/262/276/347/273/206/345/214/226/346/272/220/347/240/201/345/256/241/346/237/245-20260718.md +3 -3
  10. package/_packed_docs/aun/345/205/254/347/275/221/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +7 -0
  11. package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +422 -331
  12. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +598 -398
  13. package/_packed_docs/group-message-rpc-alignment-gaps.md +741 -0
  14. package/_packed_docs/message-online-push-alignment.md +572 -0
  15. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1 -1
  16. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +10 -10
  17. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +2 -2
  18. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +30 -10
  19. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +23 -20
  20. package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +3 -3
  21. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +5 -6
  22. package/_packed_docs/sdk/09-group-rpc-manual.md +11 -7
  23. package/_packed_docs/sdk/09-message-rpc-manual.md +5 -3
  24. package/_packed_docs/sdk/09-storage-rpc-manual.md +14 -3
  25. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +19 -17
  26. package/_packed_docs/sdk/CHANGELOG-0.5.6.md +73 -0
  27. package/_packed_docs/sdk/INDEX.md +22 -22
  28. package/_packed_docs/sdk/README.md +6 -5
  29. package/_packed_docs//345/217/221/345/270/203/346/212/245/345/221/212-0.5.6.md +260 -0
  30. package/dist/agent-md-schema.d.ts +14 -0
  31. package/dist/agent-md-schema.d.ts.map +1 -0
  32. package/dist/agent-md-schema.js +103 -0
  33. package/dist/agent-md-schema.js.map +1 -0
  34. package/dist/agent-md.d.ts +7 -0
  35. package/dist/agent-md.d.ts.map +1 -1
  36. package/dist/agent-md.js +229 -10
  37. package/dist/agent-md.js.map +1 -1
  38. package/dist/aid-store.d.ts +5 -1
  39. package/dist/aid-store.d.ts.map +1 -1
  40. package/dist/aid-store.js +26 -4
  41. package/dist/aid-store.js.map +1 -1
  42. package/dist/aid.d.ts +2 -0
  43. package/dist/aid.d.ts.map +1 -1
  44. package/dist/aid.js +29 -7
  45. package/dist/aid.js.map +1 -1
  46. package/dist/auth.d.ts.map +1 -1
  47. package/dist/auth.js +1 -0
  48. package/dist/auth.js.map +1 -1
  49. package/dist/bundle.js +29895 -21968
  50. package/dist/cert-utils.d.ts +1 -0
  51. package/dist/cert-utils.d.ts.map +1 -1
  52. package/dist/cert-utils.js +36 -16
  53. package/dist/cert-utils.js.map +1 -1
  54. package/dist/client/delivery.d.ts +33 -5
  55. package/dist/client/delivery.d.ts.map +1 -1
  56. package/dist/client/delivery.js +597 -220
  57. package/dist/client/delivery.js.map +1 -1
  58. package/dist/client/lifecycle.d.ts +15 -1
  59. package/dist/client/lifecycle.d.ts.map +1 -1
  60. package/dist/client/lifecycle.js +452 -151
  61. package/dist/client/lifecycle.js.map +1 -1
  62. package/dist/client/rpc-pipeline.d.ts +17 -6
  63. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  64. package/dist/client/rpc-pipeline.js +284 -115
  65. package/dist/client/rpc-pipeline.js.map +1 -1
  66. package/dist/client/runtime.d.ts +1 -3
  67. package/dist/client/runtime.d.ts.map +1 -1
  68. package/dist/client/runtime.js +4 -7
  69. package/dist/client/runtime.js.map +1 -1
  70. package/dist/client/v2-e2ee.d.ts +20 -2
  71. package/dist/client/v2-e2ee.d.ts.map +1 -1
  72. package/dist/client/v2-e2ee.js +398 -114
  73. package/dist/client/v2-e2ee.js.map +1 -1
  74. package/dist/client.d.ts +22 -10
  75. package/dist/client.d.ts.map +1 -1
  76. package/dist/client.js +396 -104
  77. package/dist/client.js.map +1 -1
  78. package/dist/facades.d.ts +23 -1
  79. package/dist/facades.d.ts.map +1 -1
  80. package/dist/facades.js +51 -7
  81. package/dist/facades.js.map +1 -1
  82. package/dist/group-id.d.ts.map +1 -1
  83. package/dist/group-id.js +2 -17
  84. package/dist/group-id.js.map +1 -1
  85. package/dist/index.d.ts +4 -3
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +2 -1
  88. package/dist/index.js.map +1 -1
  89. package/dist/seq-tracker.d.ts.map +1 -1
  90. package/dist/seq-tracker.js +8 -2
  91. package/dist/seq-tracker.js.map +1 -1
  92. package/dist/tools/cross-sdk-agent.js +23 -1
  93. package/dist/tools/cross-sdk-agent.js.map +1 -1
  94. package/dist/transport.d.ts.map +1 -1
  95. package/dist/transport.js +8 -1
  96. package/dist/transport.js.map +1 -1
  97. package/dist/validators.d.ts.map +1 -1
  98. package/dist/validators.js +8 -14
  99. package/dist/validators.js.map +1 -1
  100. package/dist/version.d.ts +1 -1
  101. package/dist/version.js +1 -1
  102. package/package.json +6 -5
@@ -57,6 +57,25 @@ function positiveSafeSequenceHint(value) {
57
57
  const parsed = Number(value);
58
58
  return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 0;
59
59
  }
60
+ function nonNegativeSafeSequenceHint(value) {
61
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
62
+ return null;
63
+ return value;
64
+ }
65
+ function deduplicatePlainForwardPageMessages(value, afterSeq) {
66
+ if (!Array.isArray(value))
67
+ return [];
68
+ const bySeq = new Map();
69
+ for (const raw of value) {
70
+ if (!isJsonObject(raw))
71
+ continue;
72
+ const seq = positiveSafeSequenceHint(raw.seq);
73
+ if (seq <= afterSeq || bySeq.has(seq))
74
+ continue;
75
+ bySeq.set(seq, { ...raw, seq });
76
+ }
77
+ return [...bySeq.entries()].sort(([left], [right]) => left - right).map(([, message]) => message);
78
+ }
60
79
  function p2pAppEventFromPlainPullMessage(message) {
61
80
  const payload = message.payload;
62
81
  const payloadType = isJsonObject(payload)
@@ -77,18 +96,24 @@ export class MessageDeliveryEngine {
77
96
  runtime;
78
97
  realtimeTailResults = null;
79
98
  realtimeSyncing = null;
99
+ pendingP2pPullUpper = null;
100
+ pendingGroupPullUpper = null;
80
101
  realtimeAcking = null;
81
102
  pendingP2PInlineAcks = null;
82
103
  pendingGroupInlineAcks = null;
83
104
  pendingGroupInlineAckNamespaces = null;
84
105
  // reset 后旧 inline Promise 只能自行结束,不能触碰新身份上下文的状态。
85
106
  inlineGeneration = 0;
107
+ // 同一实例的 tracker 写入必须串行,避免旧的延迟 flush 覆盖新的 Forward 提交。
108
+ seqTrackerWriteChain = Promise.resolve();
86
109
  constructor(runtime) {
87
110
  this.runtime = runtime;
88
111
  }
89
112
  resetInlineAckState() {
90
113
  this.inlineGeneration += 1;
91
114
  this.realtimeSyncing = null;
115
+ this.pendingP2pPullUpper = null;
116
+ this.pendingGroupPullUpper = null;
92
117
  this.realtimeTailResults = null;
93
118
  this.realtimeAcking = null;
94
119
  this.pendingP2PInlineAcks = null;
@@ -112,6 +137,61 @@ export class MessageDeliveryEngine {
112
137
  endRealtimeSync(ns) {
113
138
  this.realtimeSyncing?.delete(ns);
114
139
  }
140
+ hasPendingPull(ns) {
141
+ const pending = ns.startsWith('p2p:') ? this.pendingP2pPullUpper : this.pendingGroupPullUpper;
142
+ return (pending?.get(ns) ?? 0) > 0;
143
+ }
144
+ onPullGateIdle(ns) {
145
+ if (!ns)
146
+ return;
147
+ this.schedulePendingPullIfNeeded(ns, 'pull-gate-idle');
148
+ }
149
+ recordPendingPull(ns, seq) {
150
+ if (!ns || !Number.isSafeInteger(seq) || seq <= 0)
151
+ return;
152
+ const pending = ns.startsWith('p2p:')
153
+ ? (this.pendingP2pPullUpper ?? new Map())
154
+ : (this.pendingGroupPullUpper ?? new Map());
155
+ if (ns.startsWith('p2p:'))
156
+ this.pendingP2pPullUpper = pending;
157
+ else
158
+ this.pendingGroupPullUpper = pending;
159
+ pending.set(ns, Math.max(pending.get(ns) ?? 0, seq));
160
+ }
161
+ consumePendingPull(ns, throughSeq) {
162
+ const pending = ns.startsWith('p2p:') ? this.pendingP2pPullUpper : this.pendingGroupPullUpper;
163
+ if ((pending?.get(ns) ?? 0) <= throughSeq)
164
+ pending?.delete(ns);
165
+ }
166
+ schedulePendingPullIfNeeded(ns, reason) {
167
+ const pending = ns.startsWith('p2p:') ? this.pendingP2pPullUpper : this.pendingGroupPullUpper;
168
+ if (!pending)
169
+ return false;
170
+ const upper = pending.get(ns) ?? 0;
171
+ if (upper <= 0) {
172
+ pending.delete(ns);
173
+ return false;
174
+ }
175
+ const client = this.runtime.client;
176
+ const contiguous = client._seqTracker.getContiguousSeq(ns);
177
+ if (upper <= contiguous) {
178
+ pending.delete(ns);
179
+ client._clientLog?.debug(`pending pull upper already covered: ns=${ns}, upper_seq=${upper}, contiguous=${contiguous}, reason=${reason}`);
180
+ return false;
181
+ }
182
+ if (client.state !== ConnectionState.READY || client._closing || this.realtimeSyncing?.has(ns)
183
+ || client._rpcPipeline?.hasPullActivity?.(ns, false))
184
+ return false;
185
+ pending.delete(ns);
186
+ client._clientLog?.debug(`pending push follow-up pull scheduled: ns=${ns}, upper_seq=${upper}, reason=${reason}`);
187
+ if (ns.startsWith('p2p:')) {
188
+ client._safeAsync(this.onV2PushNotification({ seq: upper }));
189
+ }
190
+ else {
191
+ client._safeAsync(this.onRawGroupV2MessageCreated({ group_id: ns.slice('group:'.length), seq: upper }));
192
+ }
193
+ return true;
194
+ }
115
195
  beginRealtimeAck(ns, seq) {
116
196
  if (!ns || !Number.isSafeInteger(seq) || seq <= 0)
117
197
  return false;
@@ -499,6 +579,204 @@ export class MessageDeliveryEngine {
499
579
  }
500
580
  return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
501
581
  }
582
+ resolveP2PPullAckSeq(result, requestedSeq) {
583
+ if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
584
+ return 0;
585
+ if (isJsonObject(result)
586
+ && Object.prototype.hasOwnProperty.call(result, 'server_ack_seq')) {
587
+ const value = result.server_ack_seq;
588
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
589
+ }
590
+ return 0;
591
+ }
592
+ resolveGroupPullAckSeq(result, requestedSeq) {
593
+ if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
594
+ return 0;
595
+ if (!isJsonObject(result))
596
+ return 0;
597
+ const row = result;
598
+ if (!Object.prototype.hasOwnProperty.call(row, 'cursor'))
599
+ return 0;
600
+ const cursor = row.cursor;
601
+ if (!isJsonObject(cursor))
602
+ return 0;
603
+ const cursorRow = cursor;
604
+ if (!Object.prototype.hasOwnProperty.call(cursorRow, 'current_seq'))
605
+ return 0;
606
+ const value = cursorRow.current_seq;
607
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
608
+ }
609
+ resolveForwardAckSeq(result, requestedSeq) {
610
+ if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
611
+ return 0;
612
+ // 旧服务端成功响应可能不带累计水位字段;RPC 已成功,兼容按请求水位处理。
613
+ if (!isJsonObject(result))
614
+ return requestedSeq;
615
+ const row = result;
616
+ let value;
617
+ if (Object.prototype.hasOwnProperty.call(row, 'effective_ack_seq')) {
618
+ value = row.effective_ack_seq;
619
+ }
620
+ else if (Object.prototype.hasOwnProperty.call(row, 'ack_seq')) {
621
+ value = row.ack_seq;
622
+ }
623
+ else {
624
+ return requestedSeq;
625
+ }
626
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
627
+ }
628
+ forwardCoordinator() {
629
+ const coordinator = this.runtime.client._v2E2EE;
630
+ if (!coordinator
631
+ || typeof coordinator.pendingForwardCursor !== 'function'
632
+ || typeof coordinator.recordForwardCursor !== 'function'
633
+ || typeof coordinator.clearForwardCursor !== 'function'
634
+ || typeof coordinator.pendingForwardAck !== 'function'
635
+ || typeof coordinator.recordForwardAck !== 'function'
636
+ || typeof coordinator.clearForwardAck !== 'function') {
637
+ throw new Error('Forward cursor coordinator is unavailable');
638
+ }
639
+ return coordinator;
640
+ }
641
+ async confirmPlainForwardAck(ns, method, ackSeq, groupId = '') {
642
+ const client = this.runtime.client;
643
+ const coordinator = this.forwardCoordinator();
644
+ const generation = this.captureInlineGeneration();
645
+ coordinator.recordForwardAck(ns, ackSeq);
646
+ const params = method === 'message.ack'
647
+ ? {
648
+ seq: ackSeq,
649
+ device_id: client._deviceId,
650
+ slot_id: client._slotId,
651
+ _rpc_background: true,
652
+ }
653
+ : {
654
+ group_id: groupId,
655
+ msg_seq: ackSeq,
656
+ device_id: client._deviceId,
657
+ slot_id: client._slotId,
658
+ _rpc_background: true,
659
+ };
660
+ const result = await client._rpcPipeline.rawCall(method, params, { background: true });
661
+ const actualAckSeq = this.resolveForwardAckSeq(result, ackSeq);
662
+ if (actualAckSeq < ackSeq) {
663
+ throw new Error(`${method} server ACK watermark ${actualAckSeq} is below requested ${ackSeq}`);
664
+ }
665
+ coordinator.clearForwardAck(ns, actualAckSeq);
666
+ if (method === 'message.ack') {
667
+ this.completePendingP2PInlineAckUpTo(ns, actualAckSeq, generation);
668
+ }
669
+ else {
670
+ this.completePendingGroupInlineAckUpTo(ns, actualAckSeq, generation);
671
+ }
672
+ }
673
+ async commitPlainForwardPage(ns, afterSeq, method, result, groupId = '') {
674
+ if (!isJsonObject(result)) {
675
+ throw new Error(`${method} response must be an object`);
676
+ }
677
+ const client = this.runtime.client;
678
+ const response = result;
679
+ const messages = deduplicatePlainForwardPageMessages(response.messages, afterSeq);
680
+ const coordinator = this.forwardCoordinator();
681
+ const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
682
+ const pageInlineGeneration = this.captureInlineGeneration();
683
+ const deferredServerCursor = Number(coordinator.pendingForwardCursor(ns, pageContigBefore) ?? 0);
684
+ const deferredForwardAck = Number(coordinator.pendingForwardAck(ns, pageContigBefore) ?? 0);
685
+ const pageTrackerSnapshot = typeof client._seqTracker.snapshotNamespace === 'function'
686
+ ? client._seqTracker.snapshotNamespace(ns)
687
+ : null;
688
+ response._contig_before = pageContigBefore;
689
+ const cursor = isJsonObject(response.cursor)
690
+ ? response.cursor
691
+ : null;
692
+ const serverAckRaw = method === 'message.pull' ? response.server_ack_seq : cursor?.current_seq;
693
+ const hasServerAck = method === 'message.pull'
694
+ ? Object.prototype.hasOwnProperty.call(response, 'server_ack_seq')
695
+ : Boolean(cursor && Object.prototype.hasOwnProperty.call(cursor, 'current_seq'));
696
+ const parsedServerAck = hasServerAck ? nonNegativeSafeSequenceHint(serverAckRaw) : null;
697
+ const serverAckSeq = parsedServerAck ?? 0;
698
+ const retentionFloor = method === 'message.pull'
699
+ ? positiveSafeSequenceHint(response.retention_floor_seq)
700
+ : Math.max(positiveSafeSequenceHint(response.retention_floor_message_seq), positiveSafeSequenceHint(response.retention_floor_seq), positiveSafeSequenceHint(cursor?.retention_floor_seq));
701
+ const visibilityFloor = method === 'group.pull'
702
+ ? Math.max(positiveSafeSequenceHint(cursor?.join_seq), Math.max(0, positiveSafeSequenceHint(response.earliest_available_message_seq) - 1))
703
+ : 0;
704
+ let publishedCount = 0;
705
+ let committed = false;
706
+ try {
707
+ for (const rawMessage of messages) {
708
+ const seq = positiveSafeSequenceHint(rawMessage.seq);
709
+ if (method === 'message.pull') {
710
+ const appEvent = p2pAppEventFromPlainPullMessage(rawMessage);
711
+ if (await this.publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false)) {
712
+ publishedCount += 1;
713
+ }
714
+ continue;
715
+ }
716
+ const message = normalizeGroupMentionMode(rawMessage);
717
+ if (this.recallEventFromGroupMessage(message)) {
718
+ if (await this.publishGroupRecallTombstone(groupId, seq, message)) {
719
+ this.markPublishedSeq(ns, seq);
720
+ publishedCount += 1;
721
+ }
722
+ }
723
+ else if (await this.publishPulledMessage('group.message_created', ns, seq, message, false)) {
724
+ publishedCount += 1;
725
+ }
726
+ }
727
+ if (messages.length > 0)
728
+ client._seqTracker.onPullResult(ns, messages, afterSeq);
729
+ const commitTarget = Math.max(client._seqTracker.getContiguousSeq(ns), retentionFloor, visibilityFloor, deferredServerCursor);
730
+ if (commitTarget > client._seqTracker.getContiguousSeq(ns)) {
731
+ client._seqTracker.forceContiguousSeq(ns, commitTarget);
732
+ }
733
+ if (client._seqTracker.getContiguousSeq(ns) !== pageContigBefore) {
734
+ await this.drainOrderedMessages(ns, undefined, false, false);
735
+ await client._commitSeqTrackerState(ns);
736
+ }
737
+ committed = true;
738
+ }
739
+ catch (exc) {
740
+ if (!committed && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
741
+ client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
742
+ this.dropSeqTrackerPending(ns);
743
+ }
744
+ throw exc;
745
+ }
746
+ const committedAck = client._seqTracker.getContiguousSeq(ns);
747
+ if (deferredServerCursor > 0 && committedAck >= deferredServerCursor) {
748
+ coordinator.clearForwardCursor(ns, committedAck);
749
+ }
750
+ if (hasServerAck && parsedServerAck !== null && serverAckSeq > committedAck) {
751
+ coordinator.recordForwardCursor(ns, serverAckSeq, committedAck);
752
+ }
753
+ if (hasServerAck && parsedServerAck !== null && serverAckSeq >= deferredForwardAck) {
754
+ coordinator.clearForwardAck(ns, serverAckSeq);
755
+ if (method === 'message.pull') {
756
+ this.completePendingP2PInlineAckUpTo(ns, serverAckSeq, pageInlineGeneration);
757
+ }
758
+ else {
759
+ this.completePendingGroupInlineAckUpTo(ns, serverAckSeq, pageInlineGeneration);
760
+ }
761
+ }
762
+ const contigAdvanced = committedAck > pageContigBefore;
763
+ const knownServerAck = hasServerAck ? serverAckSeq : deferredServerCursor;
764
+ if (committedAck > 0
765
+ && ((hasServerAck && committedAck > serverAckSeq)
766
+ || (contigAdvanced && committedAck > knownServerAck))) {
767
+ coordinator.recordForwardAck(ns, committedAck);
768
+ }
769
+ const pendingAckSeq = Number(coordinator.pendingForwardAck(ns, committedAck) ?? 0);
770
+ if (pendingAckSeq > 0) {
771
+ const ackMethod = method === 'message.pull' ? 'message.ack' : 'group.ack_messages';
772
+ const clampedAckSeq = this.clampAckSeq(ackMethod, ackMethod === 'message.ack' ? 'seq' : 'msg_seq', ns, pendingAckSeq);
773
+ if (clampedAckSeq < pendingAckSeq) {
774
+ throw new Error(`${ackMethod} cannot confirm pending Forward watermark ${pendingAckSeq}`);
775
+ }
776
+ await this.confirmPlainForwardAck(ns, ackMethod, pendingAckSeq, groupId);
777
+ }
778
+ return { rawCount: messages.length, publishedCount };
779
+ }
502
780
  enqueueOrderedMessage(ns, event, seq, payload) {
503
781
  const client = this.runtime.client;
504
782
  let queue = client._pendingOrderedMsgs.get(ns);
@@ -1432,55 +1710,47 @@ export class MessageDeliveryEngine {
1432
1710
  }
1433
1711
  const ns = this.resolvePendingGroupInlineAckNamespace(`group:${groupId}`);
1434
1712
  const afterSeq = client._seqTracker.getContiguousSeq(ns);
1435
- try {
1436
- if (client._v2Session) {
1713
+ if (client._v2Session) {
1714
+ try {
1437
1715
  const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 });
1438
1716
  await client._rpcPipeline.runPullSerialized(key, () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true }), true);
1439
1717
  return;
1440
1718
  }
1441
- const result = await client.call('group.pull', {
1719
+ catch (exc) {
1720
+ client._clientLog.warn(`auto pull group message failed:${String(exc)}`);
1721
+ await client._publishAppEvent('group.message_created', notification);
1722
+ return;
1723
+ }
1724
+ }
1725
+ let rawCompleted = false;
1726
+ try {
1727
+ const key = client._rpcPipeline.pullGateKeyForCall('group.pull', {
1442
1728
  group_id: groupId,
1443
1729
  after_message_seq: afterSeq,
1444
- device_id: client._deviceId,
1445
1730
  limit: 50,
1446
- _rpc_background: true,
1447
1731
  });
1448
- if (isJsonObject(result)) {
1449
- const messages = result.messages;
1450
- if (Array.isArray(messages)) {
1451
- const pushed = client._pushedSeqs.get(ns);
1452
- for (const msg of messages) {
1453
- if (isJsonObject(msg)) {
1454
- const s = msg.seq;
1455
- if (pushed && s !== undefined && s !== null && pushed.has(s)) {
1456
- continue;
1457
- }
1458
- // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
1459
- // 与 V2 pull 路径(v2-e2ee.ts pullGroupV2)对齐,避免 legacy 回退路径把撤回
1460
- // tombstone 当普通 group.message_created 投递给应用层。
1461
- if (s !== undefined && s !== null
1462
- && this.recallEventFromGroupMessage(msg)) {
1463
- await this.publishGroupRecallTombstone(groupId, s, msg);
1464
- this.markPublishedSeq(ns, Number(s));
1465
- continue;
1466
- }
1467
- if (s !== undefined && s !== null) {
1468
- await client._publishPulledMessage('group.message_created', ns, s, msg);
1469
- }
1470
- else {
1471
- await client._publishAppEvent('group.message_created', msg);
1472
- }
1473
- }
1474
- }
1475
- this.prunePushedSeqs(ns);
1476
- return;
1477
- }
1478
- }
1732
+ await client._rpcPipeline.runPullSerialized(key, async () => {
1733
+ const result = await client._rpcPipeline.rawCall('group.pull', {
1734
+ group_id: groupId,
1735
+ after_message_seq: afterSeq,
1736
+ device_id: client._deviceId,
1737
+ slot_id: client._slotId,
1738
+ limit: 50,
1739
+ _rpc_background: true,
1740
+ }, { background: true });
1741
+ rawCompleted = true;
1742
+ await this.commitPlainForwardPage(ns, afterSeq, 'group.pull', result, groupId);
1743
+ this.prunePushedSeqs(ns);
1744
+ }, true);
1479
1745
  }
1480
1746
  catch (exc) {
1481
- client._clientLog.warn(`auto pull group message failed:${String(exc)}`);
1747
+ // 页发布或本地提交失败不能 ACK;提交后 ACK 失败则保留 pending。
1748
+ // 两种情况都不能再把原始通知当作第二条业务消息发布。
1749
+ client._clientLog.warn(`auto pull group message commit/ack failed:${String(exc)}`);
1750
+ if (!rawCompleted) {
1751
+ await client._publishAppEvent('group.message_created', notification);
1752
+ }
1482
1753
  }
1483
- await client._publishAppEvent('group.message_created', notification);
1484
1754
  }
1485
1755
  scheduleP2pGapRetry(ns, retryAttempt) {
1486
1756
  const client = this.runtime.client;
@@ -1541,6 +1811,7 @@ export class MessageDeliveryEngine {
1541
1811
  finally {
1542
1812
  client._gapFillDone.delete(dedupKey);
1543
1813
  this.runtime.delivery.setGapFillActive(false);
1814
+ this.schedulePendingPullIfNeeded(ns, 'gap-fill-complete');
1544
1815
  }
1545
1816
  }
1546
1817
  async fillP2pGapWithPlainPull(ns, afterSeq) {
@@ -1552,25 +1823,9 @@ export class MessageDeliveryEngine {
1552
1823
  slot_id: client._slotId,
1553
1824
  _rpc_background: true,
1554
1825
  };
1555
- const raw = await client._transport.call('message.pull', params, undefined, undefined, true);
1556
- const result = typeof client._rpcPipeline?.postprocessResult === 'function'
1557
- ? await client._rpcPipeline.postprocessResult('message.pull', params, raw)
1558
- : raw;
1559
- const messages = (isJsonObject(result) && Array.isArray(result.messages))
1560
- ? result.messages
1561
- : [];
1562
- let published = 0;
1563
- for (const item of messages) {
1564
- if (!isJsonObject(item))
1565
- continue;
1566
- const msg = item;
1567
- const seq = Number(msg.seq ?? 0);
1568
- const appEvent = p2pAppEventFromPlainPullMessage(msg);
1569
- if (await this.publishPulledMessage(appEvent.event, ns, seq, appEvent.payload)) {
1570
- published += 1;
1571
- }
1572
- }
1573
- return published;
1826
+ const raw = await client._rpcPipeline.rawCall('message.pull', params, { background: true });
1827
+ const page = await this.commitPlainForwardPage(ns, afterSeq, 'message.pull', raw);
1828
+ return page.publishedCount;
1574
1829
  }
1575
1830
  async fillGroupGap(groupId) {
1576
1831
  const client = this.runtime.client;
@@ -1604,9 +1859,10 @@ export class MessageDeliveryEngine {
1604
1859
  if (filled > 0 && client._seqTracker.getContiguousSeq(ns) > afterSeq) {
1605
1860
  client._safeAsync(this.fillGroupGap(groupId));
1606
1861
  }
1862
+ this.schedulePendingPullIfNeeded(ns, 'gap-fill-complete');
1607
1863
  }
1608
1864
  }
1609
- async fillGroupEventGap(groupId) {
1865
+ async fillGroupEventGap(groupId, options) {
1610
1866
  const client = this.runtime.client;
1611
1867
  if (client._sessionOptions?.background_sync === false)
1612
1868
  return;
@@ -1616,25 +1872,31 @@ export class MessageDeliveryEngine {
1616
1872
  if (!groupId)
1617
1873
  return;
1618
1874
  const ns = `group_event:${groupId}`;
1619
- const afterSeq = client._seqTracker.getContiguousSeq(ns);
1875
+ const requestedAfterSeq = Number(options?.afterSeq ?? 0);
1876
+ const afterSeq = Number.isSafeInteger(requestedAfterSeq) && requestedAfterSeq > 0
1877
+ ? requestedAfterSeq
1878
+ : client._seqTracker.getContiguousSeq(ns);
1879
+ const singlePage = options?.singlePage === true;
1620
1880
  const dedupKey = `group_event_pull:${ns}`;
1621
1881
  if (client._gapFillDone.has(dedupKey))
1622
1882
  return;
1623
1883
  client._gapFillDone.add(dedupKey);
1624
1884
  this.runtime.delivery.setGapFillActive(true);
1885
+ let continuationAfterSeq = 0;
1625
1886
  try {
1626
1887
  let nextAfterSeq = afterSeq;
1627
- const maxPages = 100;
1888
+ const maxPages = singlePage ? 1 : 100;
1628
1889
  let pageCount = 0;
1629
1890
  while (pageCount < maxPages) {
1630
1891
  pageCount += 1;
1631
- const result = await client.call('group.pull_events', {
1892
+ const request = {
1632
1893
  group_id: groupId,
1633
1894
  after_event_seq: nextAfterSeq,
1634
1895
  device_id: client._deviceId,
1635
1896
  limit: 50,
1636
1897
  _rpc_background: true,
1637
- });
1898
+ };
1899
+ const result = await client.call('group.pull_events', request);
1638
1900
  if (!isJsonObject(result))
1639
1901
  return;
1640
1902
  const events = result.events;
@@ -1690,16 +1952,28 @@ export class MessageDeliveryEngine {
1690
1952
  this.persistSeq(ns);
1691
1953
  }
1692
1954
  if (eventObjects.length > 0 && ackContig > 0 && ackContig !== pageContigBefore) {
1693
- client._transport.call('group.ack_events', {
1694
- group_id: groupId,
1695
- event_seq: ackContig,
1696
- device_id: client._deviceId,
1697
- slot_id: client._slotId,
1698
- _rpc_background: true,
1699
- }).catch((e) => { client._clientLog.warn('group event auto-ack failed: group=' + groupId, e); });
1955
+ try {
1956
+ const ackPromise = client._transport.call('group.ack_events', {
1957
+ group_id: groupId,
1958
+ event_seq: ackContig,
1959
+ device_id: client._deviceId,
1960
+ slot_id: client._slotId,
1961
+ _rpc_background: true,
1962
+ });
1963
+ if (singlePage)
1964
+ await ackPromise;
1965
+ else
1966
+ ackPromise.catch((e) => { client._clientLog.warn('group event auto-ack failed: group=' + groupId, e); });
1967
+ }
1968
+ catch (e) {
1969
+ client._clientLog.warn('group event auto-ack failed: group=' + groupId, e);
1970
+ }
1700
1971
  }
1701
1972
  const nextAfter = Math.max(eventSeqs.length > 0 ? Math.max(...eventSeqs) : nextAfterSeq, nextAfterSeq);
1702
- if (eventObjects.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
1973
+ if (singlePage && result.has_more === true && nextAfter > nextAfterSeq) {
1974
+ continuationAfterSeq = nextAfter;
1975
+ }
1976
+ if (eventObjects.length === 0 || nextAfter <= nextAfterSeq || result.has_more !== true)
1703
1977
  break;
1704
1978
  nextAfterSeq = nextAfter;
1705
1979
  }
@@ -1713,6 +1987,12 @@ export class MessageDeliveryEngine {
1713
1987
  finally {
1714
1988
  client._gapFillDone.delete(dedupKey);
1715
1989
  this.runtime.delivery.setGapFillActive(false);
1990
+ if (singlePage && continuationAfterSeq > afterSeq) {
1991
+ this.enqueueOnlineUnreadEventHint({
1992
+ group_id: groupId,
1993
+ _online_hint_after_event_seq: continuationAfterSeq,
1994
+ });
1995
+ }
1716
1996
  }
1717
1997
  }
1718
1998
  async handleGroupChangedEventSeq(data, groupId) {
@@ -1731,8 +2011,8 @@ export class MessageDeliveryEngine {
1731
2011
  client._clientLog.debug(`group.changed online unread hint skipped: group=${groupId} background_sync=false`);
1732
2012
  return;
1733
2013
  }
1734
- client._clientLog.debug(`group.changed online unread hint triggering gap fill: group=${groupId}, event_seq=${eventSeq}`);
1735
- client._safeAsync(this.fillGroupEventGap(groupId));
2014
+ client._clientLog.debug(`group.changed online unread hint triggering event Forward: group=${groupId}, event_seq=${eventSeq}`);
2015
+ this.enqueueOnlineUnreadEventHint(data);
1736
2016
  return;
1737
2017
  }
1738
2018
  if (this.isSelfJoinGroupChanged(data)) {
@@ -1794,50 +2074,59 @@ export class MessageDeliveryEngine {
1794
2074
  const groupId = String(data.group_id ?? '').trim();
1795
2075
  if (!groupId)
1796
2076
  return;
1797
- client._onlineUnreadHintQueue.set(groupId, { ...data });
1798
- if (client._onlineUnreadHintTimer || client._onlineUnreadHintDrainActive)
1799
- return;
1800
- const delayMs = Math.max(0, Number(client._onlineUnreadHintInitialDelayMs ?? 750) || 0);
1801
- this.runtime.delivery.setOnlineUnreadHintTimer(setTimeout(() => {
1802
- this.runtime.delivery.setOnlineUnreadHintTimer(null);
1803
- client._safeAsync(this.drainOnlineUnreadHints());
1804
- }, delayMs));
2077
+ const ns = `group:${groupId}`;
2078
+ client._safeAsync(this.runGroupForwardRecovery(groupId, ns, 50, true).catch((exc) => {
2079
+ client._clientLog?.debug(`online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
2080
+ }));
1805
2081
  }
1806
- async drainOnlineUnreadHints() {
2082
+ enqueueOnlineUnreadEventHint(data) {
1807
2083
  const client = this.runtime.client;
1808
- if (client._onlineUnreadHintDrainActive)
2084
+ const groupId = String(data.group_id ?? '').trim();
2085
+ if (!groupId || client._sessionOptions?.background_sync === false)
1809
2086
  return;
1810
- this.runtime.delivery.setOnlineUnreadHintDrainActive(true);
1811
- const attemptedAckByGroup = new Map();
1812
- try {
1813
- while (client._onlineUnreadHintQueue.size > 0) {
1814
- if (client.state !== ConnectionState.READY)
1815
- return;
1816
- const groupId = client._onlineUnreadHintQueue.keys().next().value;
1817
- if (!groupId)
1818
- return;
1819
- const payload = client._onlineUnreadHintQueue.get(groupId);
1820
- client._onlineUnreadHintQueue.delete(groupId);
1821
- if (!payload)
1822
- continue;
1823
- const currentAck = this.syncState(`group:${groupId}`).ack;
1824
- const attemptedAck = attemptedAckByGroup.get(groupId);
1825
- if (attemptedAck !== undefined && currentAck <= attemptedAck)
1826
- continue;
1827
- attemptedAckByGroup.set(groupId, currentAck);
1828
- await this.onRawGroupV2MessageCreated({ ...payload, _online_hint_drained: true });
1829
- const intervalMs = Math.max(0, Number(client._onlineUnreadHintIntervalMs ?? 50) || 0);
1830
- if (intervalMs > 0 && client._onlineUnreadHintQueue.size > 0) {
1831
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
1832
- }
1833
- }
1834
- }
1835
- catch (exc) {
1836
- client._clientLog.debug(`online unread hint drain failed: ${formatDeliveryError(exc)}`);
1837
- }
1838
- finally {
1839
- this.runtime.delivery.setOnlineUnreadHintDrainActive(false);
1840
- }
2087
+ const rawAfter = Number(data._online_hint_after_event_seq ?? 0);
2088
+ const afterSeq = Number.isSafeInteger(rawAfter) && rawAfter > 0 ? rawAfter : 0;
2089
+ client._safeAsync(this.fillGroupEventGap(groupId, {
2090
+ afterSeq,
2091
+ singlePage: true,
2092
+ }));
2093
+ }
2094
+ async runP2PForwardRecovery(ns, pageLimit, background = false) {
2095
+ const client = this.runtime.client;
2096
+ const request = {
2097
+ after_seq: this.syncState(ns).ack,
2098
+ limit: pageLimit,
2099
+ max_pages: 1,
2100
+ };
2101
+ const invoke = async () => {
2102
+ const after = this.syncState(ns).ack;
2103
+ const messages = await client._pullV2(after, pageLimit, { gateLocked: true, maxPages: 1 });
2104
+ return { messages, raw_count: messages.length };
2105
+ };
2106
+ const pipeline = client._rpcPipeline;
2107
+ if (!pipeline)
2108
+ return await invoke();
2109
+ const key = pipeline.pullGateKeyForCall('message.pull', request);
2110
+ return await pipeline.runPullSerialized(key, invoke, background);
2111
+ }
2112
+ async runGroupForwardRecovery(groupId, ns, pageLimit, background = false) {
2113
+ const client = this.runtime.client;
2114
+ const request = {
2115
+ group_id: groupId,
2116
+ after_seq: this.syncState(ns).ack,
2117
+ limit: pageLimit,
2118
+ max_pages: 1,
2119
+ };
2120
+ const invoke = async () => {
2121
+ const after = this.syncState(ns).ack;
2122
+ const messages = await client._pullGroupV2(groupId, after, pageLimit, { gateLocked: true, maxPages: 1 });
2123
+ return { messages, raw_count: messages.length };
2124
+ };
2125
+ const pipeline = client._rpcPipeline;
2126
+ if (!pipeline)
2127
+ return await invoke();
2128
+ const key = pipeline.pullGateKeyForCall('group.pull', request);
2129
+ return await pipeline.runPullSerialized(key, invoke, background);
1841
2130
  }
1842
2131
  syncState(ns) {
1843
2132
  const tracker = this.runtime.client._seqTracker;
@@ -2253,8 +2542,10 @@ export class MessageDeliveryEngine {
2253
2542
  }
2254
2543
  }
2255
2544
  hasOutstandingRealtimeGap(ns) {
2256
- const maxSeenSeq = this.runtime.client._seqTracker.getMaxSeenSeq(ns);
2257
- return Number.isSafeInteger(maxSeenSeq) && maxSeenSeq > this.syncState(ns).ack;
2545
+ const state = this.syncState(ns);
2546
+ // Tail 已建立可靠后缀窗口;后台续拉只针对尚未接上窗口的真实缺口。
2547
+ // maxSeen 可能只是 Tail 期间到达的通知,不能单独据此启动全量补洞。
2548
+ return state.ack < state.tail - 1;
2258
2549
  }
2259
2550
  scheduleP2PRealtimeFullGapFill(ns) {
2260
2551
  const client = this.runtime.client;
@@ -2307,7 +2598,17 @@ export class MessageDeliveryEngine {
2307
2598
  }
2308
2599
  async runP2PRealtimeSync(ns, pushSeq, pageLimit = 50, forceTail = false) {
2309
2600
  const client = this.runtime.client;
2310
- const order = client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit);
2601
+ const before = this.syncState(ns);
2602
+ const order = forceTail ? ['tail'] : typeof client._seqTracker.planRealtimeSync === 'function'
2603
+ ? client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit)
2604
+ : (() => {
2605
+ const gap = pushSeq - before.ack;
2606
+ if (gap <= 0)
2607
+ return before.ack < before.tail - 1 ? ['forward'] : [];
2608
+ return gap <= pageLimit * 2 ? ['forward'] : ['tail'];
2609
+ })();
2610
+ // Planner 依据 S-A 选路;H 已覆盖 S 也不代表 A 已连续提交,仍需前台 Forward。
2611
+ const headForward = order[0] === 'forward' && pushSeq > before.ack;
2311
2612
  let result = { messages: [], raw_count: 0 };
2312
2613
  let tailCompleted = false;
2313
2614
  const run = async (tail, background, maxPages, forceForward = false, bypassCoveredHead = false) => {
@@ -2322,7 +2623,7 @@ export class MessageDeliveryEngine {
2322
2623
  if (tail) {
2323
2624
  const tailResult = await client._v2E2EE.pullV2TailInternal({
2324
2625
  window_mode: 'tail', after_seq: live.ack, limit: pageLimit,
2325
- });
2626
+ }, true);
2326
2627
  tailCompleted = true;
2327
2628
  return tailResult;
2328
2629
  }
@@ -2341,22 +2642,38 @@ export class MessageDeliveryEngine {
2341
2642
  // 落盘的 ack 为起点,至多补一页普通 Forward 即可恢复该小窗口。
2342
2643
  const tailMissedDuringPull = tailCompleted
2343
2644
  && Number(client._seqTracker.getMaxSeenSeq(ns) || 0) > state.head;
2344
- let boundedForwardForMissedHead = false;
2345
- if (client._sessionOptions?.background_sync !== false
2346
- && (order[0] === 'forward'
2347
- || (tailCompleted && (state.ack < state.tail - 1 || tailMissedDuringPull)))) {
2348
- result = await run(false, true, order[0] === 'tail' ? 1 : undefined, tailMissedDuringPull);
2349
- boundedForwardForMissedHead = order[0] === 'tail' && tailMissedDuringPull;
2350
- }
2351
- if (boundedForwardForMissedHead)
2645
+ const pendingThroughSeq = tailMissedDuringPull
2646
+ ? Number(client._seqTracker.getMaxSeenSeq(ns) || 0)
2647
+ : 0;
2648
+ const tailForward = tailCompleted && (state.ack < state.tail - 1 || tailMissedDuringPull);
2649
+ const backgroundWindowForward = order[0] === 'forward' && !headForward;
2650
+ if (headForward || tailForward
2651
+ || (client._sessionOptions?.background_sync !== false && backgroundWindowForward)) {
2652
+ result = await run(false, !(headForward || tailForward), headForward ? 2 : order[0] === 'tail' ? 1 : undefined, headForward || tailMissedDuringPull);
2653
+ if (pendingThroughSeq > 0)
2654
+ this.consumePendingPull(ns, pendingThroughSeq);
2655
+ }
2656
+ const finalState = this.syncState(ns);
2657
+ if (tailCompleted && finalState.ack < finalState.tail - 1) {
2352
2658
  this.scheduleP2PRealtimeFullGapFill(ns);
2659
+ }
2353
2660
  if (tailCompleted)
2354
2661
  this.markRealtimeTailCompleted(result);
2355
2662
  return result;
2356
2663
  }
2357
2664
  async runGroupRealtimeSync(groupId, ns, pushSeq, pageLimit = 50, forceTail = false) {
2358
2665
  const client = this.runtime.client;
2359
- const order = client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit);
2666
+ const before = this.syncState(ns);
2667
+ const order = forceTail ? ['tail'] : typeof client._seqTracker.planRealtimeSync === 'function'
2668
+ ? client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit)
2669
+ : (() => {
2670
+ const gap = pushSeq - before.ack;
2671
+ if (gap <= 0)
2672
+ return before.ack < before.tail - 1 ? ['forward'] : [];
2673
+ return gap <= pageLimit * 2 ? ['forward'] : ['tail'];
2674
+ })();
2675
+ // Planner 依据 S-A 选路;H 已覆盖 S 也不代表 A 已连续提交,仍需前台 Forward。
2676
+ const headForward = order[0] === 'forward' && pushSeq > before.ack;
2360
2677
  let result = { messages: [], raw_count: 0 };
2361
2678
  let tailCompleted = false;
2362
2679
  const run = async (tail, background, maxPages, forceForward = false, bypassCoveredHead = false) => {
@@ -2371,7 +2688,7 @@ export class MessageDeliveryEngine {
2371
2688
  if (tail) {
2372
2689
  const tailResult = await client._v2E2EE.pullGroupV2TailInternal({
2373
2690
  group_id: groupId, window_mode: 'tail', after_seq: live.ack, limit: pageLimit,
2374
- });
2691
+ }, true);
2375
2692
  tailCompleted = true;
2376
2693
  return tailResult;
2377
2694
  }
@@ -2389,31 +2706,25 @@ export class MessageDeliveryEngine {
2389
2706
  // 期间新到达的序号;绝不重新规划或再执行 Tail。
2390
2707
  const tailMissedDuringPull = tailCompleted
2391
2708
  && Number(client._seqTracker.getMaxSeenSeq(ns) || 0) > state.head;
2392
- let boundedForwardForMissedHead = false;
2393
- if (client._sessionOptions?.background_sync !== false
2394
- && (order[0] === 'forward'
2395
- || (tailCompleted && (state.ack < state.tail - 1 || tailMissedDuringPull)))) {
2396
- result = await run(false, true, order[0] === 'tail' ? 1 : undefined, tailMissedDuringPull);
2397
- boundedForwardForMissedHead = order[0] === 'tail' && tailMissedDuringPull;
2398
- }
2399
- if (boundedForwardForMissedHead)
2709
+ const pendingThroughSeq = tailMissedDuringPull
2710
+ ? Number(client._seqTracker.getMaxSeenSeq(ns) || 0)
2711
+ : 0;
2712
+ const tailForward = tailCompleted && (state.ack < state.tail - 1 || tailMissedDuringPull);
2713
+ const backgroundWindowForward = order[0] === 'forward' && !headForward;
2714
+ if (headForward || tailForward
2715
+ || (client._sessionOptions?.background_sync !== false && backgroundWindowForward)) {
2716
+ result = await run(false, !(headForward || tailForward), headForward ? 2 : order[0] === 'tail' ? 1 : undefined, headForward || tailMissedDuringPull);
2717
+ if (pendingThroughSeq > 0)
2718
+ this.consumePendingPull(ns, pendingThroughSeq);
2719
+ }
2720
+ const finalState = this.syncState(ns);
2721
+ if (tailCompleted && finalState.ack < finalState.tail - 1) {
2400
2722
  this.scheduleGroupRealtimeFullGapFill(groupId, ns);
2723
+ }
2401
2724
  if (tailCompleted)
2402
2725
  this.markRealtimeTailCompleted(result);
2403
2726
  return result;
2404
2727
  }
2405
- async runGroupOnlineUnreadHintForward(groupId, ns, pageLimit = 50) {
2406
- const client = this.runtime.client;
2407
- const request = { group_id: groupId, after_seq: this.syncState(ns).ack, limit: pageLimit, max_pages: 1 };
2408
- const key = client._rpcPipeline.pullGateKeyForCall('group.pull', request);
2409
- return await client._rpcPipeline.runPullSerialized(key, async () => {
2410
- const live = this.syncState(ns);
2411
- const messages = await client._pullGroupV2(groupId, live.ack, pageLimit, {
2412
- gateLocked: true, maxPages: 1,
2413
- });
2414
- return { messages, raw_count: messages.length };
2415
- }, true);
2416
- }
2417
2728
  async recoverInvalidP2PRealtimeHead(ns, rawSeq) {
2418
2729
  const client = this.runtime.client;
2419
2730
  client._clientLog.warn(`V2 P2P inline Head has invalid seq; forcing authoritative Tail: seq=${String(rawSeq)}`);
@@ -2483,21 +2794,9 @@ export class MessageDeliveryEngine {
2483
2794
  const eventKind = String(data.kind ?? '').trim();
2484
2795
  // Hint 是恢复信号而不是消息 Head:不得消费 inline、推进水位或直接 ACK。
2485
2796
  if (eventKind === 'group.online_unread_hint') {
2486
- if (!data._online_hint_drained) {
2487
- if (client._sessionOptions?.background_sync === false)
2488
- return;
2489
- this.enqueueOnlineUnreadHint(data);
2490
- return;
2491
- }
2492
2797
  if (client._sessionOptions?.background_sync === false)
2493
2798
  return;
2494
- try {
2495
- // 只从已提交 Ack 做一页普通 Forward,不能由 planner 把 hint 当 Tail 证据。
2496
- await this.runGroupOnlineUnreadHintForward(groupId, `group:${groupId}`);
2497
- }
2498
- catch (exc) {
2499
- client._clientLog.warn(`group online unread hint recovery pull failed: group=${groupId} err=${formatDeliveryError(exc)}`);
2500
- }
2799
+ this.enqueueOnlineUnreadHint(data);
2501
2800
  return;
2502
2801
  }
2503
2802
  const ns = this.resolvePendingGroupInlineAckNamespace(`group:${groupId}`);
@@ -2527,30 +2826,33 @@ export class MessageDeliveryEngine {
2527
2826
  // inline 绑定仍要求原始 seq 是严格 number,避免类型归一化后消费正文。
2528
2827
  if (strictSeq || !inlinePresent)
2529
2828
  client._seqTracker.updateMaxSeen(ns, seq);
2530
- if ((!inlinePresent || inlineMatches) && !pendingConflict) {
2531
- if (seq <= before.head && before.ack >= before.tail - 1) {
2532
- const canonicalNs = this.resolvePendingGroupInlineAckNamespace(ns);
2533
- const pending = this.pendingGroupInlineAcks?.get(canonicalNs);
2534
- if (pending && seq >= pending.seq) {
2535
- this.retryPendingGroupInlineAck(canonicalNs, pending.seq, 'covered inline Group ack', seq);
2536
- }
2537
- else {
2538
- this.fireGroupV2Ack(groupId, ns, seq, 'group v2 covered push ack', inlineMatches ? inlineGeneration : null);
2539
- }
2540
- return;
2829
+ if (seq <= before.ack && before.ack >= before.tail - 1) {
2830
+ const canonicalNs = this.resolvePendingGroupInlineAckNamespace(ns);
2831
+ const pending = this.pendingGroupInlineAcks?.get(canonicalNs);
2832
+ if (pending && seq >= pending.seq) {
2833
+ this.retryPendingGroupInlineAck(canonicalNs, pending.seq, 'covered inline Group ack', seq);
2834
+ }
2835
+ else if (!inlinePresent || inlineMatches) {
2836
+ this.fireGroupV2Ack(groupId, ns, seq, 'group v2 covered push ack', inlineMatches ? inlineGeneration : null);
2541
2837
  }
2838
+ return;
2542
2839
  }
2543
- if (!this.beginRealtimeSync(ns))
2840
+ const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
2841
+ if (pullActive || !this.beginRealtimeSync(ns)) {
2842
+ this.recordPendingPull(ns, seq);
2843
+ client._clientLog?.debug(`V2 group push queued behind in-flight realtime sync: ns=${ns} seq=${seq}`);
2544
2844
  return;
2845
+ }
2545
2846
  try {
2546
2847
  // namespace 串行区必须覆盖 Tail 与其后至多一次 Forward 的完整周期。
2547
- await this.runGroupRealtimeSync(groupId, ns, seq, 50, inlinePresent && (!inlineMatches || pendingConflict));
2848
+ await this.runGroupRealtimeSync(groupId, ns, seq, 50, false);
2548
2849
  }
2549
2850
  catch (exc) {
2550
2851
  client._clientLog.warn(`V2 group push auto-sync failed: group=${groupId} err=${formatDeliveryError(exc)}`);
2551
2852
  }
2552
2853
  finally {
2553
2854
  this.endRealtimeSync(ns);
2855
+ this.schedulePendingPullIfNeeded(ns, 'push-auto-pull-complete');
2554
2856
  }
2555
2857
  }
2556
2858
  async onV2PushNotification(data) {
@@ -2558,6 +2860,12 @@ export class MessageDeliveryEngine {
2558
2860
  const ns = client._aid ? `p2p:${client._aid}` : '';
2559
2861
  if (!ns)
2560
2862
  return;
2863
+ if (isJsonObject(data) && String(data.kind ?? '').trim() === 'message.online_unread_hint') {
2864
+ if (client._sessionOptions?.background_sync === false)
2865
+ return;
2866
+ this.enqueueOnlineUnreadP2pHint(data);
2867
+ return;
2868
+ }
2561
2869
  const pushSeq = isJsonObject(data) && typeof data.seq === 'number' ? data.seq : 0;
2562
2870
  if (client._v2Session && Number.isSafeInteger(pushSeq) && pushSeq > 0) {
2563
2871
  // 与 Group 相同,在排队前暴露更高 Head 给当前在途 Tail。
@@ -2606,30 +2914,42 @@ export class MessageDeliveryEngine {
2606
2914
  const before = this.syncState(ns);
2607
2915
  if (strictSeq || !inlinePresent)
2608
2916
  client._seqTracker.updateMaxSeen(ns, pushSeq);
2609
- if ((!inlinePresent || inlineMatches) && !pendingConflict) {
2610
- if (pushSeq <= before.head && before.ack >= before.tail - 1) {
2611
- const pending = this.pendingP2PInlineAcks?.get(ns);
2612
- if (pending && pushSeq >= pending.seq) {
2613
- this.retryPendingP2PInlineAck(ns, pending.seq, 'covered inline P2P ack', pushSeq);
2614
- }
2615
- else if (inlineMatches) {
2616
- this.fireP2PV2Ack(pushSeq, 'P2P inline covered push ack', inlineGeneration);
2617
- }
2618
- return;
2917
+ if (pushSeq <= before.ack && before.ack >= before.tail - 1) {
2918
+ const pending = this.pendingP2PInlineAcks?.get(ns);
2919
+ if (pending && pushSeq >= pending.seq) {
2920
+ this.retryPendingP2PInlineAck(ns, pending.seq, 'covered inline P2P ack', pushSeq);
2921
+ }
2922
+ else if (inlineMatches) {
2923
+ this.fireP2PV2Ack(pushSeq, 'P2P inline covered push ack', inlineGeneration);
2619
2924
  }
2925
+ return;
2620
2926
  }
2621
- if (!this.beginRealtimeSync(ns))
2927
+ const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
2928
+ if (pullActive || !this.beginRealtimeSync(ns)) {
2929
+ this.recordPendingPull(ns, pushSeq);
2930
+ client._clientLog?.debug(`V2 P2P push queued behind in-flight realtime sync: ns=${ns} seq=${pushSeq}`);
2622
2931
  return;
2932
+ }
2623
2933
  try {
2624
- await this.runP2PRealtimeSync(ns, pushSeq, 50, inlinePresent && (!inlineMatches || pendingConflict));
2934
+ await this.runP2PRealtimeSync(ns, pushSeq, 50, false);
2625
2935
  }
2626
2936
  catch (exc) {
2627
2937
  client._clientLog.warn(`V2 P2P push auto-sync failed: ${formatDeliveryError(exc)}`);
2628
2938
  }
2629
2939
  finally {
2630
2940
  this.endRealtimeSync(ns);
2941
+ this.schedulePendingPullIfNeeded(ns, 'push-auto-pull-complete');
2631
2942
  }
2632
2943
  }
2944
+ enqueueOnlineUnreadP2pHint(data) {
2945
+ const client = this.runtime.client;
2946
+ if (!client._aid || !client._v2Session)
2947
+ return;
2948
+ const ns = `p2p:${client._aid}`;
2949
+ client._safeAsync(this.runP2PForwardRecovery(ns, 50, true).catch((exc) => {
2950
+ client._clientLog?.debug(`P2P online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
2951
+ }));
2952
+ }
2633
2953
  async restoreSeqTrackerState() {
2634
2954
  const client = this.runtime.client;
2635
2955
  if (!client._aid)
@@ -2745,8 +3065,14 @@ export class MessageDeliveryEngine {
2745
3065
  if (![state.ack, state.tail, state.head].some((value) => Number(value) > 0))
2746
3066
  return;
2747
3067
  const [aid, deviceId, slotId] = JSON.parse(context);
2748
- await this.writeSeqTrackerState(aid, deviceId, slotId, { [ns]: state }, true);
2749
- this.dropSeqTrackerPending(ns);
3068
+ const pendingBeforeCommit = this.takeSeqTrackerPending(context, ns);
3069
+ try {
3070
+ await this.writeSeqTrackerState(aid, deviceId, slotId, { [ns]: state }, true);
3071
+ }
3072
+ catch (exc) {
3073
+ this.restoreSeqTrackerPending(context, ns, pendingBeforeCommit);
3074
+ throw exc;
3075
+ }
2750
3076
  client._clientLog.debug(`seq_tracker atomic commit: ns=${ns} A/T/H=${JSON.stringify(state)} context=${context}`);
2751
3077
  }
2752
3078
  async flushSeqTrackerPending() {
@@ -2773,6 +3099,34 @@ export class MessageDeliveryEngine {
2773
3099
  this.cancelSeqTrackerFlushTimer();
2774
3100
  }
2775
3101
  }
3102
+ takeSeqTrackerPending(context, ns) {
3103
+ const client = this.runtime.client;
3104
+ if (client._seqTrackerPendingPersistContext !== context)
3105
+ return null;
3106
+ const pending = this.seqTrackerPending();
3107
+ const state = pending.get(ns);
3108
+ if (!state)
3109
+ return null;
3110
+ pending.delete(ns);
3111
+ if (pending.size === 0) {
3112
+ client._seqTrackerPendingPersistContext = null;
3113
+ this.cancelSeqTrackerFlushTimer();
3114
+ }
3115
+ return { ...state };
3116
+ }
3117
+ restoreSeqTrackerPending(context, ns, state) {
3118
+ if (!state || this.currentSeqTrackerContext() !== context)
3119
+ return;
3120
+ const client = this.runtime.client;
3121
+ const pendingContext = client._seqTrackerPendingPersistContext;
3122
+ if (pendingContext && pendingContext !== context)
3123
+ return;
3124
+ const pending = this.seqTrackerPending();
3125
+ if (!pending.has(ns))
3126
+ pending.set(ns, state);
3127
+ client._seqTrackerPendingPersistContext = context;
3128
+ this.scheduleSeqTrackerFlush();
3129
+ }
2776
3130
  currentSeqTrackerContext() {
2777
3131
  const client = this.runtime.client;
2778
3132
  if (!client._aid)
@@ -2823,6 +3177,14 @@ export class MessageDeliveryEngine {
2823
3177
  }
2824
3178
  }
2825
3179
  async writeSeqTrackerState(aid, deviceId, slotId, state, throwOnError = false) {
3180
+ await this.enqueueSeqTrackerWrite(() => this.writeSeqTrackerStateNow(aid, deviceId, slotId, state, throwOnError));
3181
+ }
3182
+ enqueueSeqTrackerWrite(operation) {
3183
+ const run = this.seqTrackerWriteChain.then(operation, operation);
3184
+ this.seqTrackerWriteChain = run.catch(() => { });
3185
+ return run;
3186
+ }
3187
+ async writeSeqTrackerStateNow(aid, deviceId, slotId, state, throwOnError = false) {
2826
3188
  const client = this.runtime.client;
2827
3189
  if (!aid || Object.keys(state).length === 0)
2828
3190
  return;
@@ -2865,41 +3227,56 @@ export class MessageDeliveryEngine {
2865
3227
  const client = this.runtime.client;
2866
3228
  if (!client._aid || !ns)
2867
3229
  return;
3230
+ const aid = client._aid;
3231
+ const deviceId = client._deviceId;
3232
+ const slotId = client._slotId;
2868
3233
  const seq = client._seqTracker.getContiguousSeq(ns);
2869
3234
  this.dropSeqTrackerPending(ns);
2870
- try {
2871
- if (seq > 0 && typeof client._tokenStore.saveSeq === 'function') {
2872
- client._tokenStore.saveSeq(client._aid, client._deviceId, client._slotId, ns, seq).catch((exc) => {
3235
+ const saveSeq = client._tokenStore.saveSeq;
3236
+ if (seq > 0 && typeof saveSeq === 'function') {
3237
+ void this.enqueueSeqTrackerWrite(async () => {
3238
+ try {
3239
+ await saveSeq.call(client._tokenStore, aid, deviceId, slotId, ns, seq);
3240
+ }
3241
+ catch (exc) {
2873
3242
  client._clientLog.debug(`persist repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
2874
- });
2875
- return;
2876
- }
2877
- const deleteSeq = client._tokenStore.deleteSeq;
2878
- if (seq <= 0 && typeof deleteSeq === 'function') {
2879
- deleteSeq.call(client._tokenStore, client._aid, client._deviceId, client._slotId, ns).catch((exc) => {
3243
+ }
3244
+ });
3245
+ return;
3246
+ }
3247
+ const deleteSeq = client._tokenStore.deleteSeq;
3248
+ if (seq <= 0 && typeof deleteSeq === 'function') {
3249
+ void this.enqueueSeqTrackerWrite(async () => {
3250
+ try {
3251
+ await deleteSeq.call(client._tokenStore, aid, deviceId, slotId, ns);
3252
+ }
3253
+ catch (exc) {
2880
3254
  client._clientLog.debug(`delete repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
2881
- });
2882
- return;
2883
- }
2884
- if (seq <= 0 && typeof client._tokenStore.updateInstanceState === 'function') {
2885
- Promise.resolve(client._tokenStore.updateInstanceState(client._aid, client._deviceId, client._slotId, (current) => {
2886
- if (isJsonObject(current.seq_tracker_state)) {
2887
- const next = { ...current.seq_tracker_state };
2888
- delete next[ns];
2889
- current.seq_tracker_state = next;
2890
- }
2891
- return current;
2892
- })).catch((exc) => {
3255
+ }
3256
+ });
3257
+ return;
3258
+ }
3259
+ const updater = client._tokenStore.updateInstanceState;
3260
+ if (seq <= 0 && typeof updater === 'function') {
3261
+ void this.enqueueSeqTrackerWrite(async () => {
3262
+ try {
3263
+ await updater.call(client._tokenStore, aid, deviceId, slotId, (current) => {
3264
+ if (isJsonObject(current.seq_tracker_state)) {
3265
+ const next = { ...current.seq_tracker_state };
3266
+ delete next[ns];
3267
+ current.seq_tracker_state = next;
3268
+ }
3269
+ return current;
3270
+ });
3271
+ }
3272
+ catch (exc) {
2893
3273
  client._clientLog.debug(`delete repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
2894
- });
2895
- return;
2896
- }
2897
- if (seq > 0) {
2898
- void this.saveSeqTrackerState();
2899
- }
3274
+ }
3275
+ });
3276
+ return;
2900
3277
  }
2901
- catch (exc) {
2902
- client._clientLog.debug(`persist repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
3278
+ if (seq > 0) {
3279
+ void this.saveSeqTrackerState();
2903
3280
  }
2904
3281
  }
2905
3282
  clampAckSeq(method, field, ns, seq) {
@@ -2960,7 +3337,7 @@ export class MessageDeliveryEngine {
2960
3337
  }
2961
3338
  return params;
2962
3339
  }
2963
- async drainOrderedMessages(ns, beforeSeq, pullResponse = false) {
3340
+ async drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true) {
2964
3341
  const client = this.runtime.client;
2965
3342
  const queue = client._pendingOrderedMsgs.get(ns);
2966
3343
  if (!queue || queue.size === 0)
@@ -2981,7 +3358,7 @@ export class MessageDeliveryEngine {
2981
3358
  }
2982
3359
  if (queue.size === 0) {
2983
3360
  client._pendingOrderedMsgs.delete(ns);
2984
- if (delivered)
3361
+ if (delivered && persist)
2985
3362
  await this.saveSeqTrackerState();
2986
3363
  }
2987
3364
  }
@@ -3018,7 +3395,7 @@ export class MessageDeliveryEngine {
3018
3395
  await this.saveSeqTrackerState();
3019
3396
  return true;
3020
3397
  }
3021
- async publishPulledMessage(event, ns, seq, payload) {
3398
+ async publishPulledMessage(event, ns, seq, payload, persist = true) {
3022
3399
  const client = this.runtime.client;
3023
3400
  const seqNum = Number(seq);
3024
3401
  if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0 || !ns) {
@@ -3035,7 +3412,7 @@ export class MessageDeliveryEngine {
3035
3412
  client._pendingOrderedMsgs.delete(ns);
3036
3413
  return false;
3037
3414
  }
3038
- await this.drainOrderedMessages(ns, seqNum);
3415
+ await this.drainOrderedMessages(ns, seqNum, false, persist);
3039
3416
  queue?.delete(seqNum);
3040
3417
  if (queue && queue.size === 0)
3041
3418
  client._pendingOrderedMsgs.delete(ns);