@agentunion/fastaun-browser 0.5.6 → 0.5.9

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 (104) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/_packed_docs/CHANGELOG.md +104 -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/agent.md/examples//347/276/244/347/273/204-/345/274/200/345/217/221/345/233/242/351/230/237.md +21 -0
  10. 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
  11. 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
  12. 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
  13. 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
  14. package/_packed_docs/group-message-rpc-alignment-gaps.md +741 -0
  15. package/_packed_docs/message-online-push-alignment.md +572 -0
  16. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1 -1
  17. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +10 -10
  18. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +2 -2
  19. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +30 -10
  20. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +23 -20
  21. package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +3 -3
  22. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +5 -6
  23. package/_packed_docs/sdk/09-group-rpc-manual.md +11 -7
  24. package/_packed_docs/sdk/09-message-rpc-manual.md +5 -3
  25. package/_packed_docs/sdk/09-storage-rpc-manual.md +14 -3
  26. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +19 -17
  27. package/_packed_docs/sdk/CHANGELOG-0.5.6.md +73 -0
  28. package/_packed_docs/sdk/INDEX.md +22 -22
  29. package/_packed_docs/sdk/README.md +6 -5
  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 +230 -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 +24 -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 +30253 -21955
  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 +40 -5
  55. package/dist/client/delivery.d.ts.map +1 -1
  56. package/dist/client/delivery.js +719 -221
  57. package/dist/client/delivery.js.map +1 -1
  58. package/dist/client/group-state.js +2 -2
  59. package/dist/client/group-state.js.map +1 -1
  60. package/dist/client/lifecycle.d.ts +15 -1
  61. package/dist/client/lifecycle.d.ts.map +1 -1
  62. package/dist/client/lifecycle.js +463 -151
  63. package/dist/client/lifecycle.js.map +1 -1
  64. package/dist/client/rpc-pipeline.d.ts +25 -6
  65. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  66. package/dist/client/rpc-pipeline.js +407 -123
  67. package/dist/client/rpc-pipeline.js.map +1 -1
  68. package/dist/client/runtime.d.ts +1 -3
  69. package/dist/client/runtime.d.ts.map +1 -1
  70. package/dist/client/runtime.js +4 -7
  71. package/dist/client/runtime.js.map +1 -1
  72. package/dist/client/v2-e2ee.d.ts +20 -2
  73. package/dist/client/v2-e2ee.d.ts.map +1 -1
  74. package/dist/client/v2-e2ee.js +434 -112
  75. package/dist/client/v2-e2ee.js.map +1 -1
  76. package/dist/client.d.ts +22 -10
  77. package/dist/client.d.ts.map +1 -1
  78. package/dist/client.js +500 -116
  79. package/dist/client.js.map +1 -1
  80. package/dist/facades.d.ts +23 -1
  81. package/dist/facades.d.ts.map +1 -1
  82. package/dist/facades.js +55 -7
  83. package/dist/facades.js.map +1 -1
  84. package/dist/group-id.d.ts.map +1 -1
  85. package/dist/group-id.js +2 -17
  86. package/dist/group-id.js.map +1 -1
  87. package/dist/index.d.ts +4 -3
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +2 -1
  90. package/dist/index.js.map +1 -1
  91. package/dist/seq-tracker.d.ts.map +1 -1
  92. package/dist/seq-tracker.js +8 -2
  93. package/dist/seq-tracker.js.map +1 -1
  94. package/dist/tools/cross-sdk-agent.js +23 -1
  95. package/dist/tools/cross-sdk-agent.js.map +1 -1
  96. package/dist/transport.d.ts.map +1 -1
  97. package/dist/transport.js +8 -1
  98. package/dist/transport.js.map +1 -1
  99. package/dist/validators.d.ts.map +1 -1
  100. package/dist/validators.js +8 -14
  101. package/dist/validators.js.map +1 -1
  102. package/dist/version.d.ts +1 -1
  103. package/dist/version.js +1 -1
  104. 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,31 @@ export class MessageDeliveryEngine {
77
96
  runtime;
78
97
  realtimeTailResults = null;
79
98
  realtimeSyncing = null;
99
+ pendingP2pPullUpper = null;
100
+ pendingGroupPullUpper = null;
101
+ pendingPullNoProgressAcks = null;
102
+ onlineUnreadHintTargets = null;
103
+ onlineUnreadHintOwners = null;
80
104
  realtimeAcking = null;
81
105
  pendingP2PInlineAcks = null;
82
106
  pendingGroupInlineAcks = null;
83
107
  pendingGroupInlineAckNamespaces = null;
84
108
  // reset 后旧 inline Promise 只能自行结束,不能触碰新身份上下文的状态。
85
109
  inlineGeneration = 0;
110
+ // 同一实例的 tracker 写入必须串行,避免旧的延迟 flush 覆盖新的 Forward 提交。
111
+ seqTrackerWriteChain = Promise.resolve();
86
112
  constructor(runtime) {
87
113
  this.runtime = runtime;
88
114
  }
89
115
  resetInlineAckState() {
90
116
  this.inlineGeneration += 1;
117
+ void this.runtime.client._rpcPipeline?.invalidatePulls?.();
91
118
  this.realtimeSyncing = null;
119
+ this.pendingP2pPullUpper = null;
120
+ this.pendingGroupPullUpper = null;
121
+ this.pendingPullNoProgressAcks = null;
122
+ this.onlineUnreadHintTargets = null;
123
+ this.onlineUnreadHintOwners = null;
92
124
  this.realtimeTailResults = null;
93
125
  this.realtimeAcking = null;
94
126
  this.pendingP2PInlineAcks = null;
@@ -98,6 +130,20 @@ export class MessageDeliveryEngine {
98
130
  isInlineGenerationCurrent(generation) {
99
131
  return generation === this.inlineGeneration;
100
132
  }
133
+ isPullOperationCurrent() {
134
+ const client = this.runtime.client;
135
+ const generation = client._pullOperationGeneration;
136
+ if (generation === undefined)
137
+ return true;
138
+ const pipeline = client._rpcPipeline;
139
+ return typeof pipeline?.isPullGenerationCurrent === 'function'
140
+ ? pipeline.isPullGenerationCurrent(generation)
141
+ : true;
142
+ }
143
+ ensurePullOperationCurrent() {
144
+ if (!this.isPullOperationCurrent())
145
+ throw new Error('pull invalidated');
146
+ }
101
147
  captureInlineGeneration() {
102
148
  return this.inlineGeneration;
103
149
  }
@@ -112,6 +158,74 @@ export class MessageDeliveryEngine {
112
158
  endRealtimeSync(ns) {
113
159
  this.realtimeSyncing?.delete(ns);
114
160
  }
161
+ hasPendingPull(ns) {
162
+ const pending = ns.startsWith('p2p:') ? this.pendingP2pPullUpper : this.pendingGroupPullUpper;
163
+ return (pending?.get(ns) ?? 0) > 0;
164
+ }
165
+ onPullGateIdle(ns) {
166
+ if (!ns)
167
+ return;
168
+ this.schedulePendingPullIfNeeded(ns, 'pull-gate-idle');
169
+ }
170
+ recordPendingPull(ns, seq) {
171
+ if (!ns || !Number.isSafeInteger(seq) || seq <= 0)
172
+ return;
173
+ const pending = ns.startsWith('p2p:')
174
+ ? (this.pendingP2pPullUpper ?? new Map())
175
+ : (this.pendingGroupPullUpper ?? new Map());
176
+ if (ns.startsWith('p2p:'))
177
+ this.pendingP2pPullUpper = pending;
178
+ else
179
+ this.pendingGroupPullUpper = pending;
180
+ pending.set(ns, Math.max(pending.get(ns) ?? 0, seq));
181
+ }
182
+ consumePendingPull(ns, throughSeq) {
183
+ const pending = ns.startsWith('p2p:') ? this.pendingP2pPullUpper : this.pendingGroupPullUpper;
184
+ if ((pending?.get(ns) ?? 0) <= throughSeq) {
185
+ pending?.delete(ns);
186
+ this.pendingPullNoProgressAcks?.delete(ns);
187
+ }
188
+ }
189
+ markPendingPullNoProgress(ns, ack) {
190
+ const blocked = this.pendingPullNoProgressAcks ?? new Map();
191
+ this.pendingPullNoProgressAcks = blocked;
192
+ blocked.set(ns, ack);
193
+ }
194
+ schedulePendingPullIfNeeded(ns, reason) {
195
+ const pending = ns.startsWith('p2p:') ? this.pendingP2pPullUpper : this.pendingGroupPullUpper;
196
+ if (!pending)
197
+ return false;
198
+ const upper = pending.get(ns) ?? 0;
199
+ if (upper <= 0) {
200
+ pending.delete(ns);
201
+ this.pendingPullNoProgressAcks?.delete(ns);
202
+ return false;
203
+ }
204
+ const client = this.runtime.client;
205
+ const contiguous = client._seqTracker.getContiguousSeq(ns);
206
+ if (upper <= contiguous) {
207
+ pending.delete(ns);
208
+ this.pendingPullNoProgressAcks?.delete(ns);
209
+ client._clientLog?.debug(`pending pull upper already covered: ns=${ns}, upper_seq=${upper}, contiguous=${contiguous}, reason=${reason}`);
210
+ return false;
211
+ }
212
+ const blockedAck = this.pendingPullNoProgressAcks?.get(ns);
213
+ if (blockedAck !== undefined && contiguous <= blockedAck)
214
+ return false;
215
+ this.pendingPullNoProgressAcks?.delete(ns);
216
+ if (client.state !== ConnectionState.READY || client._closing || this.realtimeSyncing?.has(ns)
217
+ || client._rpcPipeline?.hasPullActivity?.(ns, false))
218
+ return false;
219
+ pending.delete(ns);
220
+ client._clientLog?.debug(`pending push follow-up pull scheduled: ns=${ns}, upper_seq=${upper}, reason=${reason}`);
221
+ if (ns.startsWith('p2p:')) {
222
+ client._safeAsync(this.onV2PushNotification({ seq: upper }));
223
+ }
224
+ else {
225
+ client._safeAsync(this.onRawGroupV2MessageCreated({ group_id: ns.slice('group:'.length), seq: upper }));
226
+ }
227
+ return true;
228
+ }
115
229
  beginRealtimeAck(ns, seq) {
116
230
  if (!ns || !Number.isSafeInteger(seq) || seq <= 0)
117
231
  return false;
@@ -499,6 +613,230 @@ export class MessageDeliveryEngine {
499
613
  }
500
614
  return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
501
615
  }
616
+ resolveP2PPullAckSeq(result, requestedSeq) {
617
+ if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
618
+ return 0;
619
+ if (isJsonObject(result)
620
+ && Object.prototype.hasOwnProperty.call(result, 'server_ack_seq')) {
621
+ const value = result.server_ack_seq;
622
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
623
+ }
624
+ return 0;
625
+ }
626
+ resolveGroupPullAckSeq(result, requestedSeq) {
627
+ if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
628
+ return 0;
629
+ if (!isJsonObject(result))
630
+ return 0;
631
+ const row = result;
632
+ if (!Object.prototype.hasOwnProperty.call(row, 'cursor'))
633
+ return 0;
634
+ const cursor = row.cursor;
635
+ if (!isJsonObject(cursor))
636
+ return 0;
637
+ const cursorRow = cursor;
638
+ if (!Object.prototype.hasOwnProperty.call(cursorRow, 'current_seq'))
639
+ return 0;
640
+ const value = cursorRow.current_seq;
641
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
642
+ }
643
+ resolveForwardAckSeq(result, requestedSeq) {
644
+ if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
645
+ return 0;
646
+ // 旧服务端成功响应可能不带累计水位字段;RPC 已成功,兼容按请求水位处理。
647
+ if (!isJsonObject(result))
648
+ return requestedSeq;
649
+ const row = result;
650
+ let value;
651
+ if (Object.prototype.hasOwnProperty.call(row, 'effective_ack_seq')) {
652
+ value = row.effective_ack_seq;
653
+ }
654
+ else if (Object.prototype.hasOwnProperty.call(row, 'ack_seq')) {
655
+ value = row.ack_seq;
656
+ }
657
+ else if (Object.prototype.hasOwnProperty.call(row, 'cursor')) {
658
+ const cursor = row.cursor;
659
+ const cursorRow = isJsonObject(cursor)
660
+ ? cursor
661
+ : null;
662
+ if (cursorRow) {
663
+ if (!Object.prototype.hasOwnProperty.call(cursorRow, 'current_seq'))
664
+ return 0;
665
+ value = cursorRow.current_seq;
666
+ }
667
+ else {
668
+ value = cursor;
669
+ }
670
+ }
671
+ else {
672
+ return requestedSeq;
673
+ }
674
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
675
+ }
676
+ forwardCoordinator() {
677
+ const coordinator = this.runtime.client._v2E2EE;
678
+ if (!coordinator
679
+ || typeof coordinator.pendingForwardCursor !== 'function'
680
+ || typeof coordinator.recordForwardCursor !== 'function'
681
+ || typeof coordinator.clearForwardCursor !== 'function'
682
+ || typeof coordinator.pendingForwardAck !== 'function'
683
+ || typeof coordinator.recordForwardAck !== 'function'
684
+ || typeof coordinator.clearForwardAck !== 'function') {
685
+ throw new Error('Forward cursor coordinator is unavailable');
686
+ }
687
+ return coordinator;
688
+ }
689
+ async confirmPlainForwardAck(ns, method, ackSeq, groupId = '') {
690
+ const client = this.runtime.client;
691
+ this.ensurePullOperationCurrent();
692
+ const coordinator = this.forwardCoordinator();
693
+ const generation = this.captureInlineGeneration();
694
+ coordinator.recordForwardAck(ns, ackSeq);
695
+ const params = method === 'message.ack'
696
+ ? {
697
+ seq: ackSeq,
698
+ device_id: client._deviceId,
699
+ slot_id: client._slotId,
700
+ _rpc_background: true,
701
+ }
702
+ : {
703
+ group_id: groupId,
704
+ msg_seq: ackSeq,
705
+ device_id: client._deviceId,
706
+ slot_id: client._slotId,
707
+ _rpc_background: true,
708
+ };
709
+ const result = await client._rpcPipeline.rawCall(method, params, { background: true });
710
+ this.ensurePullOperationCurrent();
711
+ const actualAckSeq = this.resolveForwardAckSeq(result, ackSeq);
712
+ if (actualAckSeq < ackSeq) {
713
+ throw new Error(`${method} server ACK watermark ${actualAckSeq} is below requested ${ackSeq}`);
714
+ }
715
+ coordinator.clearForwardAck(ns, actualAckSeq);
716
+ if (method === 'message.ack') {
717
+ this.completePendingP2PInlineAckUpTo(ns, actualAckSeq, generation);
718
+ }
719
+ else {
720
+ this.completePendingGroupInlineAckUpTo(ns, actualAckSeq, generation);
721
+ }
722
+ }
723
+ async commitPlainForwardPage(ns, afterSeq, method, result, groupId = '') {
724
+ if (!isJsonObject(result)) {
725
+ throw new Error(`${method} response must be an object`);
726
+ }
727
+ const client = this.runtime.client;
728
+ this.ensurePullOperationCurrent();
729
+ const response = result;
730
+ const messages = deduplicatePlainForwardPageMessages(response.messages, afterSeq);
731
+ const coordinator = this.forwardCoordinator();
732
+ const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
733
+ const pageInlineGeneration = this.captureInlineGeneration();
734
+ const deferredServerCursor = Number(coordinator.pendingForwardCursor(ns, pageContigBefore) ?? 0);
735
+ const deferredForwardAck = Number(coordinator.pendingForwardAck(ns, pageContigBefore) ?? 0);
736
+ const pageTrackerSnapshot = typeof client._seqTracker.snapshotNamespace === 'function'
737
+ ? client._seqTracker.snapshotNamespace(ns)
738
+ : null;
739
+ response._contig_before = pageContigBefore;
740
+ const cursor = isJsonObject(response.cursor)
741
+ ? response.cursor
742
+ : null;
743
+ const serverAckRaw = method === 'message.pull' ? response.server_ack_seq : cursor?.current_seq;
744
+ const hasServerAck = method === 'message.pull'
745
+ ? Object.prototype.hasOwnProperty.call(response, 'server_ack_seq')
746
+ : Boolean(cursor && Object.prototype.hasOwnProperty.call(cursor, 'current_seq'));
747
+ const parsedServerAck = hasServerAck ? nonNegativeSafeSequenceHint(serverAckRaw) : null;
748
+ const serverAckSeq = parsedServerAck ?? 0;
749
+ const retentionFloor = method === 'message.pull'
750
+ ? positiveSafeSequenceHint(response.retention_floor_seq)
751
+ : Math.max(positiveSafeSequenceHint(response.retention_floor_message_seq), positiveSafeSequenceHint(response.retention_floor_seq), positiveSafeSequenceHint(cursor?.retention_floor_seq));
752
+ const visibilityFloor = method === 'group.pull'
753
+ ? Math.max(positiveSafeSequenceHint(cursor?.join_seq), Math.max(0, positiveSafeSequenceHint(response.earliest_available_message_seq) - 1))
754
+ : 0;
755
+ let publishedCount = 0;
756
+ let committed = false;
757
+ try {
758
+ for (const rawMessage of messages) {
759
+ this.ensurePullOperationCurrent();
760
+ const seq = positiveSafeSequenceHint(rawMessage.seq);
761
+ if (method === 'message.pull') {
762
+ const appEvent = p2pAppEventFromPlainPullMessage(rawMessage);
763
+ if (await this.publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false)) {
764
+ publishedCount += 1;
765
+ }
766
+ this.ensurePullOperationCurrent();
767
+ continue;
768
+ }
769
+ const message = normalizeGroupMentionMode(rawMessage);
770
+ if (this.recallEventFromGroupMessage(message)) {
771
+ if (await this.publishGroupRecallTombstone(groupId, seq, message)) {
772
+ this.ensurePullOperationCurrent();
773
+ this.markPublishedSeq(ns, seq);
774
+ publishedCount += 1;
775
+ }
776
+ }
777
+ else if (await this.publishPulledMessage('group.message_created', ns, seq, message, false)) {
778
+ this.ensurePullOperationCurrent();
779
+ publishedCount += 1;
780
+ }
781
+ }
782
+ this.ensurePullOperationCurrent();
783
+ if (messages.length > 0)
784
+ client._seqTracker.onPullResult(ns, messages, afterSeq);
785
+ const commitTarget = Math.max(client._seqTracker.getContiguousSeq(ns), retentionFloor, visibilityFloor, deferredServerCursor);
786
+ if (commitTarget > client._seqTracker.getContiguousSeq(ns)) {
787
+ client._seqTracker.forceContiguousSeq(ns, commitTarget);
788
+ }
789
+ if (client._seqTracker.getContiguousSeq(ns) !== pageContigBefore) {
790
+ this.ensurePullOperationCurrent();
791
+ await this.drainOrderedMessages(ns, undefined, false, false);
792
+ this.ensurePullOperationCurrent();
793
+ await client._commitSeqTrackerState(ns);
794
+ }
795
+ committed = true;
796
+ }
797
+ catch (exc) {
798
+ if (this.isPullOperationCurrent() && !committed && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
799
+ client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
800
+ this.dropSeqTrackerPending(ns);
801
+ }
802
+ throw exc;
803
+ }
804
+ this.ensurePullOperationCurrent();
805
+ const committedAck = client._seqTracker.getContiguousSeq(ns);
806
+ if (deferredServerCursor > 0 && committedAck >= deferredServerCursor) {
807
+ coordinator.clearForwardCursor(ns, committedAck);
808
+ }
809
+ if (hasServerAck && parsedServerAck !== null && serverAckSeq > committedAck) {
810
+ coordinator.recordForwardCursor(ns, serverAckSeq, committedAck);
811
+ }
812
+ if (hasServerAck && parsedServerAck !== null && serverAckSeq >= deferredForwardAck) {
813
+ coordinator.clearForwardAck(ns, serverAckSeq);
814
+ if (method === 'message.pull') {
815
+ this.completePendingP2PInlineAckUpTo(ns, serverAckSeq, pageInlineGeneration);
816
+ }
817
+ else {
818
+ this.completePendingGroupInlineAckUpTo(ns, serverAckSeq, pageInlineGeneration);
819
+ }
820
+ }
821
+ const contigAdvanced = committedAck > pageContigBefore;
822
+ const knownServerAck = hasServerAck ? serverAckSeq : deferredServerCursor;
823
+ if (committedAck > 0
824
+ && ((hasServerAck && committedAck > serverAckSeq)
825
+ || (contigAdvanced && committedAck > knownServerAck))) {
826
+ coordinator.recordForwardAck(ns, committedAck);
827
+ }
828
+ const pendingAckSeq = Number(coordinator.pendingForwardAck(ns, committedAck) ?? 0);
829
+ if (pendingAckSeq > 0) {
830
+ const ackMethod = method === 'message.pull' ? 'message.ack' : 'group.ack_messages';
831
+ const clampedAckSeq = this.clampAckSeq(ackMethod, ackMethod === 'message.ack' ? 'seq' : 'msg_seq', ns, pendingAckSeq);
832
+ if (clampedAckSeq < pendingAckSeq) {
833
+ throw new Error(`${ackMethod} cannot confirm pending Forward watermark ${pendingAckSeq}`);
834
+ }
835
+ this.ensurePullOperationCurrent();
836
+ await this.confirmPlainForwardAck(ns, ackMethod, pendingAckSeq, groupId);
837
+ }
838
+ return { rawCount: messages.length, publishedCount };
839
+ }
502
840
  enqueueOrderedMessage(ns, event, seq, payload) {
503
841
  const client = this.runtime.client;
504
842
  let queue = client._pendingOrderedMsgs.get(ns);
@@ -1432,55 +1770,47 @@ export class MessageDeliveryEngine {
1432
1770
  }
1433
1771
  const ns = this.resolvePendingGroupInlineAckNamespace(`group:${groupId}`);
1434
1772
  const afterSeq = client._seqTracker.getContiguousSeq(ns);
1435
- try {
1436
- if (client._v2Session) {
1773
+ if (client._v2Session) {
1774
+ try {
1437
1775
  const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 });
1438
1776
  await client._rpcPipeline.runPullSerialized(key, () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true }), true);
1439
1777
  return;
1440
1778
  }
1441
- const result = await client.call('group.pull', {
1779
+ catch (exc) {
1780
+ client._clientLog.warn(`auto pull group message failed:${String(exc)}`);
1781
+ await client._publishAppEvent('group.message_created', notification);
1782
+ return;
1783
+ }
1784
+ }
1785
+ let rawCompleted = false;
1786
+ try {
1787
+ const key = client._rpcPipeline.pullGateKeyForCall('group.pull', {
1442
1788
  group_id: groupId,
1443
1789
  after_message_seq: afterSeq,
1444
- device_id: client._deviceId,
1445
1790
  limit: 50,
1446
- _rpc_background: true,
1447
1791
  });
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
- }
1792
+ await client._rpcPipeline.runPullSerialized(key, async () => {
1793
+ const result = await client._rpcPipeline.rawCall('group.pull', {
1794
+ group_id: groupId,
1795
+ after_message_seq: afterSeq,
1796
+ device_id: client._deviceId,
1797
+ slot_id: client._slotId,
1798
+ limit: 50,
1799
+ _rpc_background: true,
1800
+ }, { background: true });
1801
+ rawCompleted = true;
1802
+ await this.commitPlainForwardPage(ns, afterSeq, 'group.pull', result, groupId);
1803
+ this.prunePushedSeqs(ns);
1804
+ }, true);
1479
1805
  }
1480
1806
  catch (exc) {
1481
- client._clientLog.warn(`auto pull group message failed:${String(exc)}`);
1807
+ // 页发布或本地提交失败不能 ACK;提交后 ACK 失败则保留 pending。
1808
+ // 两种情况都不能再把原始通知当作第二条业务消息发布。
1809
+ client._clientLog.warn(`auto pull group message commit/ack failed:${String(exc)}`);
1810
+ if (!rawCompleted) {
1811
+ await client._publishAppEvent('group.message_created', notification);
1812
+ }
1482
1813
  }
1483
- await client._publishAppEvent('group.message_created', notification);
1484
1814
  }
1485
1815
  scheduleP2pGapRetry(ns, retryAttempt) {
1486
1816
  const client = this.runtime.client;
@@ -1541,6 +1871,7 @@ export class MessageDeliveryEngine {
1541
1871
  finally {
1542
1872
  client._gapFillDone.delete(dedupKey);
1543
1873
  this.runtime.delivery.setGapFillActive(false);
1874
+ this.schedulePendingPullIfNeeded(ns, 'gap-fill-complete');
1544
1875
  }
1545
1876
  }
1546
1877
  async fillP2pGapWithPlainPull(ns, afterSeq) {
@@ -1552,25 +1883,9 @@ export class MessageDeliveryEngine {
1552
1883
  slot_id: client._slotId,
1553
1884
  _rpc_background: true,
1554
1885
  };
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;
1886
+ const raw = await client._rpcPipeline.rawCall('message.pull', params, { background: true });
1887
+ const page = await this.commitPlainForwardPage(ns, afterSeq, 'message.pull', raw);
1888
+ return page.publishedCount;
1574
1889
  }
1575
1890
  async fillGroupGap(groupId) {
1576
1891
  const client = this.runtime.client;
@@ -1604,9 +1919,10 @@ export class MessageDeliveryEngine {
1604
1919
  if (filled > 0 && client._seqTracker.getContiguousSeq(ns) > afterSeq) {
1605
1920
  client._safeAsync(this.fillGroupGap(groupId));
1606
1921
  }
1922
+ this.schedulePendingPullIfNeeded(ns, 'gap-fill-complete');
1607
1923
  }
1608
1924
  }
1609
- async fillGroupEventGap(groupId) {
1925
+ async fillGroupEventGap(groupId, options) {
1610
1926
  const client = this.runtime.client;
1611
1927
  if (client._sessionOptions?.background_sync === false)
1612
1928
  return;
@@ -1616,25 +1932,31 @@ export class MessageDeliveryEngine {
1616
1932
  if (!groupId)
1617
1933
  return;
1618
1934
  const ns = `group_event:${groupId}`;
1619
- const afterSeq = client._seqTracker.getContiguousSeq(ns);
1935
+ const requestedAfterSeq = Number(options?.afterSeq ?? 0);
1936
+ const afterSeq = Number.isSafeInteger(requestedAfterSeq) && requestedAfterSeq > 0
1937
+ ? requestedAfterSeq
1938
+ : client._seqTracker.getContiguousSeq(ns);
1939
+ const singlePage = options?.singlePage === true;
1620
1940
  const dedupKey = `group_event_pull:${ns}`;
1621
1941
  if (client._gapFillDone.has(dedupKey))
1622
1942
  return;
1623
1943
  client._gapFillDone.add(dedupKey);
1624
1944
  this.runtime.delivery.setGapFillActive(true);
1945
+ let continuationAfterSeq = 0;
1625
1946
  try {
1626
1947
  let nextAfterSeq = afterSeq;
1627
- const maxPages = 100;
1948
+ const maxPages = singlePage ? 1 : 100;
1628
1949
  let pageCount = 0;
1629
1950
  while (pageCount < maxPages) {
1630
1951
  pageCount += 1;
1631
- const result = await client.call('group.pull_events', {
1952
+ const request = {
1632
1953
  group_id: groupId,
1633
1954
  after_event_seq: nextAfterSeq,
1634
1955
  device_id: client._deviceId,
1635
1956
  limit: 50,
1636
1957
  _rpc_background: true,
1637
- });
1958
+ };
1959
+ const result = await client.call('group.pull_events', request);
1638
1960
  if (!isJsonObject(result))
1639
1961
  return;
1640
1962
  const events = result.events;
@@ -1690,16 +2012,28 @@ export class MessageDeliveryEngine {
1690
2012
  this.persistSeq(ns);
1691
2013
  }
1692
2014
  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); });
2015
+ try {
2016
+ const ackPromise = client._transport.call('group.ack_events', {
2017
+ group_id: groupId,
2018
+ event_seq: ackContig,
2019
+ device_id: client._deviceId,
2020
+ slot_id: client._slotId,
2021
+ _rpc_background: true,
2022
+ });
2023
+ if (singlePage)
2024
+ await ackPromise;
2025
+ else
2026
+ ackPromise.catch((e) => { client._clientLog.warn('group event auto-ack failed: group=' + groupId, e); });
2027
+ }
2028
+ catch (e) {
2029
+ client._clientLog.warn('group event auto-ack failed: group=' + groupId, e);
2030
+ }
1700
2031
  }
1701
2032
  const nextAfter = Math.max(eventSeqs.length > 0 ? Math.max(...eventSeqs) : nextAfterSeq, nextAfterSeq);
1702
- if (eventObjects.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
2033
+ if (singlePage && result.has_more === true && nextAfter > nextAfterSeq) {
2034
+ continuationAfterSeq = nextAfter;
2035
+ }
2036
+ if (eventObjects.length === 0 || nextAfter <= nextAfterSeq || result.has_more !== true)
1703
2037
  break;
1704
2038
  nextAfterSeq = nextAfter;
1705
2039
  }
@@ -1713,6 +2047,12 @@ export class MessageDeliveryEngine {
1713
2047
  finally {
1714
2048
  client._gapFillDone.delete(dedupKey);
1715
2049
  this.runtime.delivery.setGapFillActive(false);
2050
+ if (singlePage && continuationAfterSeq > afterSeq) {
2051
+ this.enqueueOnlineUnreadEventHint({
2052
+ group_id: groupId,
2053
+ _online_hint_after_event_seq: continuationAfterSeq,
2054
+ });
2055
+ }
1716
2056
  }
1717
2057
  }
1718
2058
  async handleGroupChangedEventSeq(data, groupId) {
@@ -1731,8 +2071,8 @@ export class MessageDeliveryEngine {
1731
2071
  client._clientLog.debug(`group.changed online unread hint skipped: group=${groupId} background_sync=false`);
1732
2072
  return;
1733
2073
  }
1734
- client._clientLog.debug(`group.changed online unread hint triggering gap fill: group=${groupId}, event_seq=${eventSeq}`);
1735
- client._safeAsync(this.fillGroupEventGap(groupId));
2074
+ client._clientLog.debug(`group.changed online unread hint triggering event Forward: group=${groupId}, event_seq=${eventSeq}`);
2075
+ this.enqueueOnlineUnreadEventHint(data);
1736
2076
  return;
1737
2077
  }
1738
2078
  if (this.isSelfJoinGroupChanged(data)) {
@@ -1794,50 +2134,88 @@ export class MessageDeliveryEngine {
1794
2134
  const groupId = String(data.group_id ?? '').trim();
1795
2135
  if (!groupId)
1796
2136
  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));
2137
+ const ns = `group:${groupId}`;
2138
+ const targets = this.onlineUnreadHintTargets ?? new Map();
2139
+ const owners = this.onlineUnreadHintOwners ?? new Set();
2140
+ this.onlineUnreadHintTargets = targets;
2141
+ this.onlineUnreadHintOwners = owners;
2142
+ targets.set(ns, Math.max(targets.get(ns) ?? 0, positiveSafeSequenceHint(data.seq)));
2143
+ if (owners.has(ns))
2144
+ return;
2145
+ owners.add(ns);
2146
+ client._safeAsync((async () => {
2147
+ try {
2148
+ while (true) {
2149
+ const throughSeq = targets.get(ns) ?? 0;
2150
+ const ackBefore = this.syncState(ns).ack;
2151
+ await this.runGroupForwardRecovery(groupId, ns, 50, true, throughSeq);
2152
+ const ackAfter = this.syncState(ns).ack;
2153
+ const target = targets.get(ns) ?? 0;
2154
+ if (ackAfter >= target || ackAfter <= ackBefore)
2155
+ return;
2156
+ }
2157
+ }
2158
+ catch (exc) {
2159
+ client._clientLog?.debug(`online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
2160
+ }
2161
+ finally {
2162
+ if (this.onlineUnreadHintTargets === targets)
2163
+ targets.delete(ns);
2164
+ if (this.onlineUnreadHintOwners === owners)
2165
+ owners.delete(ns);
2166
+ }
2167
+ })());
1805
2168
  }
1806
- async drainOnlineUnreadHints() {
2169
+ enqueueOnlineUnreadEventHint(data) {
1807
2170
  const client = this.runtime.client;
1808
- if (client._onlineUnreadHintDrainActive)
2171
+ const groupId = String(data.group_id ?? '').trim();
2172
+ if (!groupId || client._sessionOptions?.background_sync === false)
1809
2173
  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
- }
2174
+ const rawAfter = Number(data._online_hint_after_event_seq ?? 0);
2175
+ const afterSeq = Number.isSafeInteger(rawAfter) && rawAfter > 0 ? rawAfter : 0;
2176
+ client._safeAsync(this.fillGroupEventGap(groupId, {
2177
+ afterSeq,
2178
+ singlePage: true,
2179
+ }));
2180
+ }
2181
+ async runP2PForwardRecovery(ns, pageLimit, background = false) {
2182
+ const client = this.runtime.client;
2183
+ const request = {
2184
+ after_seq: this.syncState(ns).ack,
2185
+ limit: pageLimit,
2186
+ max_pages: 1,
2187
+ };
2188
+ const invoke = async () => {
2189
+ const after = this.syncState(ns).ack;
2190
+ const messages = await client._pullV2(after, pageLimit, { gateLocked: true, maxPages: 1 });
2191
+ return { messages, raw_count: messages.length };
2192
+ };
2193
+ const pipeline = client._rpcPipeline;
2194
+ if (!pipeline)
2195
+ return await invoke();
2196
+ const key = pipeline.pullGateKeyForCall('message.pull', request);
2197
+ return await pipeline.runPullSerialized(key, invoke, background);
2198
+ }
2199
+ async runGroupForwardRecovery(groupId, ns, pageLimit, background = false, throughSeq = 0) {
2200
+ const client = this.runtime.client;
2201
+ const state = this.syncState(ns);
2202
+ const maxPages = this.forwardMaxPages(state.ack, throughSeq, pageLimit);
2203
+ const request = {
2204
+ group_id: groupId,
2205
+ after_seq: state.ack,
2206
+ limit: pageLimit,
2207
+ max_pages: maxPages,
2208
+ };
2209
+ const invoke = async () => {
2210
+ const after = this.syncState(ns).ack;
2211
+ const messages = await client._pullGroupV2(groupId, after, pageLimit, { gateLocked: true, maxPages });
2212
+ return { messages, raw_count: messages.length };
2213
+ };
2214
+ const pipeline = client._rpcPipeline;
2215
+ if (!pipeline)
2216
+ return await invoke();
2217
+ const key = pipeline.pullGateKeyForCall('group.pull', request);
2218
+ return await pipeline.runPullSerialized(key, invoke, background);
1841
2219
  }
1842
2220
  syncState(ns) {
1843
2221
  const tracker = this.runtime.client._seqTracker;
@@ -1846,6 +2224,10 @@ export class MessageDeliveryEngine {
1846
2224
  const ack = Number(tracker.getContiguousSeq(ns) || 0);
1847
2225
  return { ack, tail: ack, head: Math.max(ack, Number(tracker.getMaxSeenSeq?.(ns) || 0)) };
1848
2226
  }
2227
+ forwardMaxPages(ack, throughSeq, pageLimit) {
2228
+ const limit = Number.isSafeInteger(pageLimit) && pageLimit > 0 ? pageLimit : 1;
2229
+ return Math.max(1, Math.ceil(Math.max(0, throughSeq - ack) / limit));
2230
+ }
1849
2231
  inlineMessage(data) {
1850
2232
  if (!isJsonObject(data))
1851
2233
  return null;
@@ -2253,8 +2635,10 @@ export class MessageDeliveryEngine {
2253
2635
  }
2254
2636
  }
2255
2637
  hasOutstandingRealtimeGap(ns) {
2256
- const maxSeenSeq = this.runtime.client._seqTracker.getMaxSeenSeq(ns);
2257
- return Number.isSafeInteger(maxSeenSeq) && maxSeenSeq > this.syncState(ns).ack;
2638
+ const state = this.syncState(ns);
2639
+ // Tail 已建立可靠后缀窗口;后台续拉只针对尚未接上窗口的真实缺口。
2640
+ // maxSeen 可能只是 Tail 期间到达的通知,不能单独据此启动全量补洞。
2641
+ return state.ack < state.tail - 1;
2258
2642
  }
2259
2643
  scheduleP2PRealtimeFullGapFill(ns) {
2260
2644
  const client = this.runtime.client;
@@ -2307,7 +2691,17 @@ export class MessageDeliveryEngine {
2307
2691
  }
2308
2692
  async runP2PRealtimeSync(ns, pushSeq, pageLimit = 50, forceTail = false) {
2309
2693
  const client = this.runtime.client;
2310
- const order = client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit);
2694
+ const before = this.syncState(ns);
2695
+ const order = forceTail ? ['tail'] : typeof client._seqTracker.planRealtimeSync === 'function'
2696
+ ? client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit)
2697
+ : (() => {
2698
+ const gap = pushSeq - before.ack;
2699
+ if (gap <= 0)
2700
+ return before.ack < before.tail - 1 ? ['forward'] : [];
2701
+ return gap <= pageLimit * 2 ? ['forward'] : ['tail'];
2702
+ })();
2703
+ // Planner 依据 S-A 选路;H 已覆盖 S 也不代表 A 已连续提交,仍需前台 Forward。
2704
+ const headForward = order[0] === 'forward' && pushSeq > before.ack;
2311
2705
  let result = { messages: [], raw_count: 0 };
2312
2706
  let tailCompleted = false;
2313
2707
  const run = async (tail, background, maxPages, forceForward = false, bypassCoveredHead = false) => {
@@ -2322,7 +2716,7 @@ export class MessageDeliveryEngine {
2322
2716
  if (tail) {
2323
2717
  const tailResult = await client._v2E2EE.pullV2TailInternal({
2324
2718
  window_mode: 'tail', after_seq: live.ack, limit: pageLimit,
2325
- });
2719
+ }, true);
2326
2720
  tailCompleted = true;
2327
2721
  return tailResult;
2328
2722
  }
@@ -2341,22 +2735,46 @@ export class MessageDeliveryEngine {
2341
2735
  // 落盘的 ack 为起点,至多补一页普通 Forward 即可恢复该小窗口。
2342
2736
  const tailMissedDuringPull = tailCompleted
2343
2737
  && 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)
2738
+ const maxSeen = Number(client._seqTracker.getMaxSeenSeq(ns) || 0);
2739
+ const pendingThroughSeq = tailMissedDuringPull
2740
+ ? maxSeen
2741
+ : 0;
2742
+ const tailForward = tailCompleted && (state.ack < state.tail - 1 || tailMissedDuringPull);
2743
+ const backgroundWindowForward = order[0] === 'forward' && !headForward;
2744
+ if (headForward || tailForward
2745
+ || (client._sessionOptions?.background_sync !== false && backgroundWindowForward)) {
2746
+ const forwardTarget = Math.max(headForward ? Math.max(pushSeq, maxSeen) : 0, (tailForward || backgroundWindowForward) ? state.tail - 1 : 0, tailMissedDuringPull ? maxSeen : 0);
2747
+ const forwardMaxPages = this.forwardMaxPages(state.ack, forwardTarget, pageLimit);
2748
+ result = await run(false, !(headForward || tailForward), forwardMaxPages, headForward || tailMissedDuringPull);
2749
+ if (pendingThroughSeq > 0) {
2750
+ const committedAck = this.syncState(ns).ack;
2751
+ if (committedAck >= pendingThroughSeq)
2752
+ this.consumePendingPull(ns, pendingThroughSeq);
2753
+ else if (committedAck <= state.ack)
2754
+ this.markPendingPullNoProgress(ns, committedAck);
2755
+ }
2756
+ }
2757
+ const finalState = this.syncState(ns);
2758
+ if (tailCompleted && finalState.ack < finalState.tail - 1) {
2352
2759
  this.scheduleP2PRealtimeFullGapFill(ns);
2760
+ }
2353
2761
  if (tailCompleted)
2354
2762
  this.markRealtimeTailCompleted(result);
2355
2763
  return result;
2356
2764
  }
2357
2765
  async runGroupRealtimeSync(groupId, ns, pushSeq, pageLimit = 50, forceTail = false) {
2358
2766
  const client = this.runtime.client;
2359
- const order = client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit);
2767
+ const before = this.syncState(ns);
2768
+ const order = forceTail ? ['tail'] : typeof client._seqTracker.planRealtimeSync === 'function'
2769
+ ? client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit)
2770
+ : (() => {
2771
+ const gap = pushSeq - before.ack;
2772
+ if (gap <= 0)
2773
+ return before.ack < before.tail - 1 ? ['forward'] : [];
2774
+ return gap <= pageLimit * 2 ? ['forward'] : ['tail'];
2775
+ })();
2776
+ // Planner 依据 S-A 选路;H 已覆盖 S 也不代表 A 已连续提交,仍需前台 Forward。
2777
+ const headForward = order[0] === 'forward' && pushSeq > before.ack;
2360
2778
  let result = { messages: [], raw_count: 0 };
2361
2779
  let tailCompleted = false;
2362
2780
  const run = async (tail, background, maxPages, forceForward = false, bypassCoveredHead = false) => {
@@ -2371,7 +2789,7 @@ export class MessageDeliveryEngine {
2371
2789
  if (tail) {
2372
2790
  const tailResult = await client._v2E2EE.pullGroupV2TailInternal({
2373
2791
  group_id: groupId, window_mode: 'tail', after_seq: live.ack, limit: pageLimit,
2374
- });
2792
+ }, true);
2375
2793
  tailCompleted = true;
2376
2794
  return tailResult;
2377
2795
  }
@@ -2389,31 +2807,33 @@ export class MessageDeliveryEngine {
2389
2807
  // 期间新到达的序号;绝不重新规划或再执行 Tail。
2390
2808
  const tailMissedDuringPull = tailCompleted
2391
2809
  && 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)
2810
+ const maxSeen = Number(client._seqTracker.getMaxSeenSeq(ns) || 0);
2811
+ const pendingThroughSeq = tailMissedDuringPull
2812
+ ? maxSeen
2813
+ : 0;
2814
+ const tailForward = tailCompleted && (state.ack < state.tail - 1 || tailMissedDuringPull);
2815
+ const backgroundWindowForward = order[0] === 'forward' && !headForward;
2816
+ if (headForward || tailForward
2817
+ || (client._sessionOptions?.background_sync !== false && backgroundWindowForward)) {
2818
+ const forwardTarget = Math.max(headForward ? Math.max(pushSeq, maxSeen) : 0, (tailForward || backgroundWindowForward) ? state.tail - 1 : 0, tailMissedDuringPull ? maxSeen : 0);
2819
+ const forwardMaxPages = this.forwardMaxPages(state.ack, forwardTarget, pageLimit);
2820
+ result = await run(false, !(headForward || tailForward), forwardMaxPages, headForward || tailMissedDuringPull);
2821
+ if (pendingThroughSeq > 0) {
2822
+ const committedAck = this.syncState(ns).ack;
2823
+ if (committedAck >= pendingThroughSeq)
2824
+ this.consumePendingPull(ns, pendingThroughSeq);
2825
+ else if (committedAck <= state.ack)
2826
+ this.markPendingPullNoProgress(ns, committedAck);
2827
+ }
2828
+ }
2829
+ const finalState = this.syncState(ns);
2830
+ if (tailCompleted && finalState.ack < finalState.tail - 1) {
2400
2831
  this.scheduleGroupRealtimeFullGapFill(groupId, ns);
2832
+ }
2401
2833
  if (tailCompleted)
2402
2834
  this.markRealtimeTailCompleted(result);
2403
2835
  return result;
2404
2836
  }
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
2837
  async recoverInvalidP2PRealtimeHead(ns, rawSeq) {
2418
2838
  const client = this.runtime.client;
2419
2839
  client._clientLog.warn(`V2 P2P inline Head has invalid seq; forcing authoritative Tail: seq=${String(rawSeq)}`);
@@ -2483,21 +2903,9 @@ export class MessageDeliveryEngine {
2483
2903
  const eventKind = String(data.kind ?? '').trim();
2484
2904
  // Hint 是恢复信号而不是消息 Head:不得消费 inline、推进水位或直接 ACK。
2485
2905
  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
2906
  if (client._sessionOptions?.background_sync === false)
2493
2907
  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
- }
2908
+ this.enqueueOnlineUnreadHint(data);
2501
2909
  return;
2502
2910
  }
2503
2911
  const ns = this.resolvePendingGroupInlineAckNamespace(`group:${groupId}`);
@@ -2527,30 +2935,33 @@ export class MessageDeliveryEngine {
2527
2935
  // inline 绑定仍要求原始 seq 是严格 number,避免类型归一化后消费正文。
2528
2936
  if (strictSeq || !inlinePresent)
2529
2937
  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;
2938
+ if (seq <= before.ack && before.ack >= before.tail - 1) {
2939
+ const canonicalNs = this.resolvePendingGroupInlineAckNamespace(ns);
2940
+ const pending = this.pendingGroupInlineAcks?.get(canonicalNs);
2941
+ if (pending && seq >= pending.seq) {
2942
+ this.retryPendingGroupInlineAck(canonicalNs, pending.seq, 'covered inline Group ack', seq);
2943
+ }
2944
+ else if (!inlinePresent || inlineMatches) {
2945
+ this.fireGroupV2Ack(groupId, ns, seq, 'group v2 covered push ack', inlineMatches ? inlineGeneration : null);
2541
2946
  }
2947
+ return;
2542
2948
  }
2543
- if (!this.beginRealtimeSync(ns))
2949
+ const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
2950
+ if (pullActive || !this.beginRealtimeSync(ns)) {
2951
+ this.recordPendingPull(ns, seq);
2952
+ client._clientLog?.debug(`V2 group push queued behind in-flight realtime sync: ns=${ns} seq=${seq}`);
2544
2953
  return;
2954
+ }
2545
2955
  try {
2546
2956
  // namespace 串行区必须覆盖 Tail 与其后至多一次 Forward 的完整周期。
2547
- await this.runGroupRealtimeSync(groupId, ns, seq, 50, inlinePresent && (!inlineMatches || pendingConflict));
2957
+ await this.runGroupRealtimeSync(groupId, ns, seq, 50, false);
2548
2958
  }
2549
2959
  catch (exc) {
2550
2960
  client._clientLog.warn(`V2 group push auto-sync failed: group=${groupId} err=${formatDeliveryError(exc)}`);
2551
2961
  }
2552
2962
  finally {
2553
2963
  this.endRealtimeSync(ns);
2964
+ this.schedulePendingPullIfNeeded(ns, 'push-auto-pull-complete');
2554
2965
  }
2555
2966
  }
2556
2967
  async onV2PushNotification(data) {
@@ -2558,6 +2969,12 @@ export class MessageDeliveryEngine {
2558
2969
  const ns = client._aid ? `p2p:${client._aid}` : '';
2559
2970
  if (!ns)
2560
2971
  return;
2972
+ if (isJsonObject(data) && String(data.kind ?? '').trim() === 'message.online_unread_hint') {
2973
+ if (client._sessionOptions?.background_sync === false)
2974
+ return;
2975
+ this.enqueueOnlineUnreadP2pHint(data);
2976
+ return;
2977
+ }
2561
2978
  const pushSeq = isJsonObject(data) && typeof data.seq === 'number' ? data.seq : 0;
2562
2979
  if (client._v2Session && Number.isSafeInteger(pushSeq) && pushSeq > 0) {
2563
2980
  // 与 Group 相同,在排队前暴露更高 Head 给当前在途 Tail。
@@ -2606,30 +3023,42 @@ export class MessageDeliveryEngine {
2606
3023
  const before = this.syncState(ns);
2607
3024
  if (strictSeq || !inlinePresent)
2608
3025
  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;
3026
+ if (pushSeq <= before.ack && before.ack >= before.tail - 1) {
3027
+ const pending = this.pendingP2PInlineAcks?.get(ns);
3028
+ if (pending && pushSeq >= pending.seq) {
3029
+ this.retryPendingP2PInlineAck(ns, pending.seq, 'covered inline P2P ack', pushSeq);
2619
3030
  }
3031
+ else if (inlineMatches) {
3032
+ this.fireP2PV2Ack(pushSeq, 'P2P inline covered push ack', inlineGeneration);
3033
+ }
3034
+ return;
2620
3035
  }
2621
- if (!this.beginRealtimeSync(ns))
3036
+ const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
3037
+ if (pullActive || !this.beginRealtimeSync(ns)) {
3038
+ this.recordPendingPull(ns, pushSeq);
3039
+ client._clientLog?.debug(`V2 P2P push queued behind in-flight realtime sync: ns=${ns} seq=${pushSeq}`);
2622
3040
  return;
3041
+ }
2623
3042
  try {
2624
- await this.runP2PRealtimeSync(ns, pushSeq, 50, inlinePresent && (!inlineMatches || pendingConflict));
3043
+ await this.runP2PRealtimeSync(ns, pushSeq, 50, false);
2625
3044
  }
2626
3045
  catch (exc) {
2627
3046
  client._clientLog.warn(`V2 P2P push auto-sync failed: ${formatDeliveryError(exc)}`);
2628
3047
  }
2629
3048
  finally {
2630
3049
  this.endRealtimeSync(ns);
3050
+ this.schedulePendingPullIfNeeded(ns, 'push-auto-pull-complete');
2631
3051
  }
2632
3052
  }
3053
+ enqueueOnlineUnreadP2pHint(data) {
3054
+ const client = this.runtime.client;
3055
+ if (!client._aid || !client._v2Session)
3056
+ return;
3057
+ const ns = `p2p:${client._aid}`;
3058
+ client._safeAsync(this.runP2PForwardRecovery(ns, 50, true).catch((exc) => {
3059
+ client._clientLog?.debug(`P2P online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
3060
+ }));
3061
+ }
2633
3062
  async restoreSeqTrackerState() {
2634
3063
  const client = this.runtime.client;
2635
3064
  if (!client._aid)
@@ -2745,8 +3174,14 @@ export class MessageDeliveryEngine {
2745
3174
  if (![state.ack, state.tail, state.head].some((value) => Number(value) > 0))
2746
3175
  return;
2747
3176
  const [aid, deviceId, slotId] = JSON.parse(context);
2748
- await this.writeSeqTrackerState(aid, deviceId, slotId, { [ns]: state }, true);
2749
- this.dropSeqTrackerPending(ns);
3177
+ const pendingBeforeCommit = this.takeSeqTrackerPending(context, ns);
3178
+ try {
3179
+ await this.writeSeqTrackerState(aid, deviceId, slotId, { [ns]: state }, true);
3180
+ }
3181
+ catch (exc) {
3182
+ this.restoreSeqTrackerPending(context, ns, pendingBeforeCommit);
3183
+ throw exc;
3184
+ }
2750
3185
  client._clientLog.debug(`seq_tracker atomic commit: ns=${ns} A/T/H=${JSON.stringify(state)} context=${context}`);
2751
3186
  }
2752
3187
  async flushSeqTrackerPending() {
@@ -2773,6 +3208,34 @@ export class MessageDeliveryEngine {
2773
3208
  this.cancelSeqTrackerFlushTimer();
2774
3209
  }
2775
3210
  }
3211
+ takeSeqTrackerPending(context, ns) {
3212
+ const client = this.runtime.client;
3213
+ if (client._seqTrackerPendingPersistContext !== context)
3214
+ return null;
3215
+ const pending = this.seqTrackerPending();
3216
+ const state = pending.get(ns);
3217
+ if (!state)
3218
+ return null;
3219
+ pending.delete(ns);
3220
+ if (pending.size === 0) {
3221
+ client._seqTrackerPendingPersistContext = null;
3222
+ this.cancelSeqTrackerFlushTimer();
3223
+ }
3224
+ return { ...state };
3225
+ }
3226
+ restoreSeqTrackerPending(context, ns, state) {
3227
+ if (!state || this.currentSeqTrackerContext() !== context)
3228
+ return;
3229
+ const client = this.runtime.client;
3230
+ const pendingContext = client._seqTrackerPendingPersistContext;
3231
+ if (pendingContext && pendingContext !== context)
3232
+ return;
3233
+ const pending = this.seqTrackerPending();
3234
+ if (!pending.has(ns))
3235
+ pending.set(ns, state);
3236
+ client._seqTrackerPendingPersistContext = context;
3237
+ this.scheduleSeqTrackerFlush();
3238
+ }
2776
3239
  currentSeqTrackerContext() {
2777
3240
  const client = this.runtime.client;
2778
3241
  if (!client._aid)
@@ -2823,6 +3286,14 @@ export class MessageDeliveryEngine {
2823
3286
  }
2824
3287
  }
2825
3288
  async writeSeqTrackerState(aid, deviceId, slotId, state, throwOnError = false) {
3289
+ await this.enqueueSeqTrackerWrite(() => this.writeSeqTrackerStateNow(aid, deviceId, slotId, state, throwOnError));
3290
+ }
3291
+ enqueueSeqTrackerWrite(operation) {
3292
+ const run = this.seqTrackerWriteChain.then(operation, operation);
3293
+ this.seqTrackerWriteChain = run.catch(() => { });
3294
+ return run;
3295
+ }
3296
+ async writeSeqTrackerStateNow(aid, deviceId, slotId, state, throwOnError = false) {
2826
3297
  const client = this.runtime.client;
2827
3298
  if (!aid || Object.keys(state).length === 0)
2828
3299
  return;
@@ -2865,41 +3336,56 @@ export class MessageDeliveryEngine {
2865
3336
  const client = this.runtime.client;
2866
3337
  if (!client._aid || !ns)
2867
3338
  return;
3339
+ const aid = client._aid;
3340
+ const deviceId = client._deviceId;
3341
+ const slotId = client._slotId;
2868
3342
  const seq = client._seqTracker.getContiguousSeq(ns);
2869
3343
  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) => {
3344
+ const saveSeq = client._tokenStore.saveSeq;
3345
+ if (seq > 0 && typeof saveSeq === 'function') {
3346
+ void this.enqueueSeqTrackerWrite(async () => {
3347
+ try {
3348
+ await saveSeq.call(client._tokenStore, aid, deviceId, slotId, ns, seq);
3349
+ }
3350
+ catch (exc) {
2873
3351
  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) => {
3352
+ }
3353
+ });
3354
+ return;
3355
+ }
3356
+ const deleteSeq = client._tokenStore.deleteSeq;
3357
+ if (seq <= 0 && typeof deleteSeq === 'function') {
3358
+ void this.enqueueSeqTrackerWrite(async () => {
3359
+ try {
3360
+ await deleteSeq.call(client._tokenStore, aid, deviceId, slotId, ns);
3361
+ }
3362
+ catch (exc) {
2880
3363
  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) => {
3364
+ }
3365
+ });
3366
+ return;
3367
+ }
3368
+ const updater = client._tokenStore.updateInstanceState;
3369
+ if (seq <= 0 && typeof updater === 'function') {
3370
+ void this.enqueueSeqTrackerWrite(async () => {
3371
+ try {
3372
+ await updater.call(client._tokenStore, aid, deviceId, slotId, (current) => {
3373
+ if (isJsonObject(current.seq_tracker_state)) {
3374
+ const next = { ...current.seq_tracker_state };
3375
+ delete next[ns];
3376
+ current.seq_tracker_state = next;
3377
+ }
3378
+ return current;
3379
+ });
3380
+ }
3381
+ catch (exc) {
2893
3382
  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
- }
3383
+ }
3384
+ });
3385
+ return;
2900
3386
  }
2901
- catch (exc) {
2902
- client._clientLog.debug(`persist repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
3387
+ if (seq > 0) {
3388
+ void this.saveSeqTrackerState();
2903
3389
  }
2904
3390
  }
2905
3391
  clampAckSeq(method, field, ns, seq) {
@@ -2960,8 +3446,9 @@ export class MessageDeliveryEngine {
2960
3446
  }
2961
3447
  return params;
2962
3448
  }
2963
- async drainOrderedMessages(ns, beforeSeq, pullResponse = false) {
3449
+ async drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true) {
2964
3450
  const client = this.runtime.client;
3451
+ this.ensurePullOperationCurrent();
2965
3452
  const queue = client._pendingOrderedMsgs.get(ns);
2966
3453
  if (!queue || queue.size === 0)
2967
3454
  return;
@@ -2971,18 +3458,22 @@ export class MessageDeliveryEngine {
2971
3458
  .sort((a, b) => a - b);
2972
3459
  let delivered = false;
2973
3460
  for (const seq of ready) {
3461
+ this.ensurePullOperationCurrent();
2974
3462
  const item = queue.get(seq);
2975
3463
  queue.delete(seq);
2976
3464
  if (!item || client._pushedSeqs.get(ns)?.has(seq))
2977
3465
  continue;
2978
3466
  await this.publishOrderedQueueItem(ns, item.event, seq, item.payload, pullResponse);
3467
+ this.ensurePullOperationCurrent();
2979
3468
  this.markPublishedSeq(ns, seq);
2980
3469
  delivered = true;
2981
3470
  }
2982
3471
  if (queue.size === 0) {
2983
3472
  client._pendingOrderedMsgs.delete(ns);
2984
- if (delivered)
3473
+ if (delivered && persist) {
3474
+ this.ensurePullOperationCurrent();
2985
3475
  await this.saveSeqTrackerState();
3476
+ }
2986
3477
  }
2987
3478
  }
2988
3479
  async publishOrderedMessage(event, ns, seq, payload) {
@@ -3018,14 +3509,18 @@ export class MessageDeliveryEngine {
3018
3509
  await this.saveSeqTrackerState();
3019
3510
  return true;
3020
3511
  }
3021
- async publishPulledMessage(event, ns, seq, payload) {
3512
+ async publishPulledMessage(event, ns, seq, payload, persist = true) {
3022
3513
  const client = this.runtime.client;
3514
+ this.ensurePullOperationCurrent();
3023
3515
  const seqNum = Number(seq);
3024
3516
  if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0 || !ns) {
3025
3517
  if (event === 'message.recalled') {
3026
- return await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seq, payload));
3518
+ const published = await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seq, payload));
3519
+ this.ensurePullOperationCurrent();
3520
+ return published;
3027
3521
  }
3028
3522
  await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload));
3523
+ this.ensurePullOperationCurrent();
3029
3524
  return true;
3030
3525
  }
3031
3526
  const queue = client._pendingOrderedMsgs.get(ns);
@@ -3035,16 +3530,19 @@ export class MessageDeliveryEngine {
3035
3530
  client._pendingOrderedMsgs.delete(ns);
3036
3531
  return false;
3037
3532
  }
3038
- await this.drainOrderedMessages(ns, seqNum);
3533
+ await this.drainOrderedMessages(ns, seqNum, false, persist);
3534
+ this.ensurePullOperationCurrent();
3039
3535
  queue?.delete(seqNum);
3040
3536
  if (queue && queue.size === 0)
3041
3537
  client._pendingOrderedMsgs.delete(ns);
3042
3538
  if (event === 'message.recalled') {
3043
3539
  const published = await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seqNum, payload));
3540
+ this.ensurePullOperationCurrent();
3044
3541
  this.markPublishedSeq(ns, seqNum);
3045
3542
  return published;
3046
3543
  }
3047
3544
  await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload));
3545
+ this.ensurePullOperationCurrent();
3048
3546
  this.markPublishedSeq(ns, seqNum);
3049
3547
  return true;
3050
3548
  }