@agentunion/fastaun 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 (92) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/_packed_docs/CHANGELOG.md +106 -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 +16 -0
  31. package/dist/agent-md-schema.js +109 -0
  32. package/dist/agent-md-schema.js.map +1 -0
  33. package/dist/agent-md.d.ts +7 -0
  34. package/dist/agent-md.js +91 -9
  35. package/dist/agent-md.js.map +1 -1
  36. package/dist/aid-store.d.ts +5 -1
  37. package/dist/aid-store.js +25 -6
  38. package/dist/aid-store.js.map +1 -1
  39. package/dist/aid.d.ts +2 -0
  40. package/dist/aid.js +32 -12
  41. package/dist/aid.js.map +1 -1
  42. package/dist/auth.d.ts +3 -1
  43. package/dist/auth.js +65 -13
  44. package/dist/auth.js.map +1 -1
  45. package/dist/cert-utils.d.ts +2 -1
  46. package/dist/cert-utils.js +32 -16
  47. package/dist/cert-utils.js.map +1 -1
  48. package/dist/client/delivery.d.ts +36 -7
  49. package/dist/client/delivery.js +701 -216
  50. package/dist/client/delivery.js.map +1 -1
  51. package/dist/client/group-state.js +2 -2
  52. package/dist/client/group-state.js.map +1 -1
  53. package/dist/client/lifecycle.d.ts +15 -1
  54. package/dist/client/lifecycle.js +475 -152
  55. package/dist/client/lifecycle.js.map +1 -1
  56. package/dist/client/rpc-pipeline.d.ts +24 -6
  57. package/dist/client/rpc-pipeline.js +372 -131
  58. package/dist/client/rpc-pipeline.js.map +1 -1
  59. package/dist/client/runtime.d.ts +1 -3
  60. package/dist/client/runtime.js +4 -7
  61. package/dist/client/runtime.js.map +1 -1
  62. package/dist/client/v2-e2ee.d.ts +20 -3
  63. package/dist/client/v2-e2ee.js +376 -93
  64. package/dist/client/v2-e2ee.js.map +1 -1
  65. package/dist/client.d.ts +23 -10
  66. package/dist/client.js +515 -150
  67. package/dist/client.js.map +1 -1
  68. package/dist/discovery.d.ts +1 -0
  69. package/dist/discovery.js +3 -0
  70. package/dist/discovery.js.map +1 -1
  71. package/dist/facades.d.ts +23 -1
  72. package/dist/facades.js +59 -7
  73. package/dist/facades.js.map +1 -1
  74. package/dist/group-id.js +2 -17
  75. package/dist/group-id.js.map +1 -1
  76. package/dist/index.d.ts +4 -3
  77. package/dist/index.js +2 -1
  78. package/dist/index.js.map +1 -1
  79. package/dist/net.d.ts +3 -0
  80. package/dist/net.js +13 -0
  81. package/dist/net.js.map +1 -1
  82. package/dist/seq-tracker.js +6 -1
  83. package/dist/seq-tracker.js.map +1 -1
  84. package/dist/tools/cross-sdk-agent.js +29 -3
  85. package/dist/tools/cross-sdk-agent.js.map +1 -1
  86. package/dist/transport.js +8 -1
  87. package/dist/transport.js.map +1 -1
  88. package/dist/validators.js +8 -14
  89. package/dist/validators.js.map +1 -1
  90. package/dist/version.d.ts +1 -1
  91. package/dist/version.js +1 -1
  92. package/package.json +4 -3
@@ -1,6 +1,6 @@
1
1
  import * as crypto from 'node:crypto';
2
2
  import { normalizeSlotId, slotIsolationKey } from '../config.js';
3
- import { ClientSignatureError, ConnectionError, PermissionError, ValidationError } from '../errors.js';
3
+ import { ClientSignatureError, ConnectionError, PermissionError, TimeoutError, ValidationError } from '../errors.js';
4
4
  import { ConnectionState, isJsonObject, } from '../types.js';
5
5
  import { validateAIDFormat, validateGroupIDFormat } from '../validators.js';
6
6
  import { normalizeGroupMentionMode, normalizeGroupSettingsResponse } from './mention-mode.js';
@@ -71,6 +71,7 @@ const SIGNED_METHODS = new Set([
71
71
  'group.dissolve', 'group.suspend', 'group.resume',
72
72
  ]);
73
73
  const SIGNING_KEY_CACHE_MAX = 32;
74
+ const IDENTITY_ADMISSION_RETRY_DELAYS_MS = [0, 50, 100, 200];
74
75
  const signingKeyCache = new Map();
75
76
  const certFingerprintCache = new Map();
76
77
  function cacheGet(cache, key) {
@@ -108,9 +109,17 @@ function getCertFingerprint(certPem) {
108
109
  }
109
110
  /** 旧式 tryAcquirePullGate 的防并发窗口。 */
110
111
  const PULL_GATE_STALE_MS = 30000;
112
+ const PULL_GATE_OPERATION_TIMEOUT_MS = 3000;
113
+ function sameIdentityAdmissionRejection(error, original) {
114
+ const errorCode = Number(error?.code);
115
+ const originalCode = Number(original?.code);
116
+ const errorMessage = String(error instanceof Error ? error.message : error?.message ?? '').trim().toLowerCase();
117
+ const originalMessage = String(original instanceof Error ? original.message : original?.message ?? '').trim().toLowerCase();
118
+ return errorCode === -32003 && originalCode === -32003 && errorMessage === originalMessage;
119
+ }
111
120
  const NON_IDEMPOTENT_TIMEOUT_MS = 35_000;
112
121
  const NON_IDEMPOTENT_METHODS = new Set([
113
- 'message.send', 'group.send', 'group.create', 'group.invite',
122
+ 'message.send', 'mail.send', 'group.send', 'group.create', 'group.invite',
114
123
  'group.kick', 'group.remove_member', 'group.leave', 'group.dissolve',
115
124
  'group.set_settings',
116
125
  'group.update_announcement', 'group.update_rules',
@@ -145,13 +154,19 @@ export class RpcPipeline {
145
154
  runtime;
146
155
  pullGateStates = new Map();
147
156
  inlineRealtimeScopes = new Set();
148
- bypassedPullScopes = new Map();
157
+ pullGeneration = 0;
158
+ pullInvalidationWait = null;
149
159
  constructor(runtime) {
150
160
  this.runtime = runtime;
151
161
  }
152
162
  async call(method, params) {
153
163
  const client = this.runtime.client;
154
164
  const tStart = Date.now();
165
+ const requestedAckSeq = method === 'message.ack' || method === 'message.v2.ack'
166
+ ? Number(params?.seq
167
+ ?? params?.up_to_seq
168
+ ?? 0) || 0
169
+ : 0;
155
170
  client._clientLog.debug(`call enter: method=${method}`);
156
171
  try {
157
172
  const preflight = this.preflight(method, params);
@@ -166,6 +181,8 @@ export class RpcPipeline {
166
181
  delete p._pull_gate_locked;
167
182
  const skipSendResultEnvelope = Boolean(p._skip_send_result_envelope);
168
183
  delete p._skip_send_result_envelope;
184
+ const deferGroupReadyPostprocess = Boolean(p._defer_group_ready_postprocess);
185
+ delete p._defer_group_ready_postprocess;
169
186
  const pullGateKey = this.pullGateKeyForCall(method, p);
170
187
  const runWithPullResponseScope = async (operation) => {
171
188
  const scope = pullGateKey ? this.pullScopeKey(pullGateKey) : '';
@@ -174,12 +191,8 @@ export class RpcPipeline {
174
191
  return await operation();
175
192
  return await wrapper.call(client, scope, operation);
176
193
  };
177
- if (pullGateKey && this.isPullResponseProcessing(pullGateKey)) {
178
- client._clientLog.debug(`pull skipped while processing pull response: method=${method} key=${pullGateKey}`);
179
- return client._emptyPullResultForCall(method);
180
- }
181
194
  if (pullGateKey && !pullGateLocked) {
182
- const pullBackground = this.pullGateBackgroundForCall(method, p);
195
+ const pullBackground = rpcBackground && this.pullGateBackgroundForCall(method, p);
183
196
  const lockedParams = { ...p, _pull_gate_locked: true };
184
197
  if (pullBackground)
185
198
  lockedParams._rpc_background = true;
@@ -238,13 +251,10 @@ export class RpcPipeline {
238
251
  if (p.window_mode === 'tail') {
239
252
  return await runWithPullResponseScope(() => client._v2E2EE.pullV2TailInternal(p));
240
253
  }
241
- const skipAutoAck = p._skip_auto_ack === true || p.skip_auto_ack === true;
242
254
  const force = p.force === true;
243
255
  const afterSeq = Number(p.after_seq ?? 0) || 0;
244
256
  const limit = Number(p.limit ?? 50) || 50;
245
- const messages = await runWithPullResponseScope(() => (skipAutoAck
246
- ? runWithRpcPriority(() => client._pullV2(afterSeq, limit, { skipAutoAck: true, gateLocked: true, force }))
247
- : runWithRpcPriority(() => client._pullV2(afterSeq, limit, { gateLocked: true, force }))));
257
+ const messages = await runWithPullResponseScope(() => runWithRpcPriority(() => client._pullV2(afterSeq, limit, { gateLocked: true, force })));
248
258
  return { messages };
249
259
  }
250
260
  if (method === 'message.history') {
@@ -253,7 +263,11 @@ export class RpcPipeline {
253
263
  }
254
264
  if (method === 'message.ack' || method === 'message.v2.ack') {
255
265
  await client._ensureV2SessionReady('message.ack');
256
- return await runWithRpcPriority(() => client._ackV2(Number(p.seq ?? p.up_to_seq ?? 0) || undefined));
266
+ const ackSeq = requestedAckSeq || Number(p.seq ?? p.up_to_seq ?? 0) || 0;
267
+ if (ackSeq > 0 && client._aid) {
268
+ await client._delivery.waitForPendingInlineCommit(`p2p:${client._aid}`, ackSeq);
269
+ }
270
+ return await runWithRpcPriority(() => client._ackV2(ackSeq || undefined));
257
271
  }
258
272
  if (method === 'group.pull' || method === 'group.v2.pull') {
259
273
  if (!String(p.group_id ?? '').trim()) {
@@ -264,8 +278,8 @@ export class RpcPipeline {
264
278
  return await runWithPullResponseScope(() => client._v2E2EE.pullGroupV2TailInternal(p));
265
279
  }
266
280
  const hasExplicitAfterSeq = 'after_seq' in p || 'after_message_seq' in p;
267
- const cursorParams = client._explicitGroupCursorParams(p);
268
- const ownsCursor = Object.keys(cursorParams).length === 0 || client._groupCursorTargetsCurrentInstance(cursorParams);
281
+ const cursorParams = this.explicitGroupCursorParams(p);
282
+ const ownsCursor = Object.keys(cursorParams).length === 0 || this.groupCursorTargetsCurrentInstance(cursorParams);
269
283
  const pullOpts = { gateLocked: true };
270
284
  if (hasExplicitAfterSeq)
271
285
  pullOpts.explicitAfterSeq = true;
@@ -294,8 +308,8 @@ export class RpcPipeline {
294
308
  throw new ValidationError('group.ack_messages requires group_id');
295
309
  }
296
310
  await client._ensureV2SessionReady('group.ack_messages');
297
- const cursorParams = client._explicitGroupCursorParams(p);
298
- const ownsCursor = Object.keys(cursorParams).length === 0 || client._groupCursorTargetsCurrentInstance(cursorParams);
311
+ const cursorParams = this.explicitGroupCursorParams(p);
312
+ const ownsCursor = Object.keys(cursorParams).length === 0 || this.groupCursorTargetsCurrentInstance(cursorParams);
299
313
  if (method === 'group.ack_messages' && !ownsCursor) {
300
314
  return await runWithRpcPriority(() => client._rawGroupAckMessages(p));
301
315
  }
@@ -311,15 +325,19 @@ export class RpcPipeline {
311
325
  if (method === 'group.thought.get' || method === 'message.thought.get') {
312
326
  client._clientLog.debug(`thought.get transport call start: method=${method}, params=${client._debugJson(client._messageEnvelopeFieldsForDebug(p))}`);
313
327
  }
314
- let result = callTimeout
315
- ? (rpcBackground
316
- ? await client._transport.call(method, p, callTimeout, undefined, true)
317
- : await client._transport.call(method, p, callTimeout))
318
- : (rpcBackground
319
- ? await client._transport.call(method, p, undefined, undefined, true)
320
- : await client._transport.call(method, p));
328
+ const invokeTransport = () => {
329
+ const request = callTimeout
330
+ ? (rpcBackground
331
+ ? client._transport.call(method, p, callTimeout, undefined, true)
332
+ : client._transport.call(method, p, callTimeout))
333
+ : (rpcBackground
334
+ ? client._transport.call(method, p, undefined, undefined, true)
335
+ : client._transport.call(method, p));
336
+ return this.bindActivePullCancellation(method, p, request);
337
+ };
338
+ let result = await this.transportCallWithIdentityRecovery(invokeTransport, method, p);
321
339
  const sendResultModeSource = result;
322
- result = await this.postprocessResult(method, p, result);
340
+ result = await this.postprocessResult(method, p, result, { skipGroupState: deferGroupReadyPostprocess });
323
341
  if (!skipSendResultEnvelope) {
324
342
  result = client._delivery.attachSendResultEnvelope(method, p, result, Boolean(p.encrypted), sendResultModeSource);
325
343
  }
@@ -531,9 +549,9 @@ export class RpcPipeline {
531
549
  }
532
550
  else {
533
551
  mode = 'forward';
534
- cursor = `after=${String(params.after_seq ?? 0)}|force=${String(Boolean(params.force))}`;
552
+ cursor = `after=${String(params.after_seq ?? 0)}`;
535
553
  }
536
- return `p2p:${client._aid}|${mode}|${cursor}|limit=${String(params.limit)}`;
554
+ return `p2p:${client._aid}|mode=${mode}|cursor=${cursor}|force=${String(Boolean(params.force))}|limit=${String(params.limit)}`;
537
555
  }
538
556
  if (method === 'group.pull' || method === 'group.v2.pull' || method === 'group.history') {
539
557
  const groupId = String(params.group_id ?? params.group_aid ?? '').trim();
@@ -551,13 +569,17 @@ export class RpcPipeline {
551
569
  }
552
570
  else {
553
571
  mode = 'forward';
554
- cursor = `after=${String(params.after_seq ?? params.after_message_seq ?? 0)}|force=${String(Boolean(params.force))}`;
572
+ cursor = `after=${String(params.after_seq ?? params.after_message_seq ?? 0)}`;
555
573
  }
556
- return `group:${groupId}|${mode}|${cursor}|limit=${String(params.limit)}`;
574
+ const explicitCursor = this.explicitGroupCursorParams(params);
575
+ const cursorSuffix = Object.keys(explicitCursor).length > 0
576
+ ? `|cursor_params=${stableStringify(explicitCursor)}`
577
+ : '';
578
+ return `group:${groupId}|mode=${mode}|cursor=${cursor}${cursorSuffix}|force=${String(Boolean(params.force))}|limit=${String(params.limit)}`;
557
579
  }
558
580
  if (method === 'group.pull_events' && String(params.group_id ?? params.group_aid ?? '').trim()) {
559
581
  const groupId = String(params.group_id ?? params.group_aid ?? '').trim();
560
- return `group_event:${groupId}|forward|after=${String(params.after_event_seq ?? 0)}|limit=${String(params.limit)}`;
582
+ return `group_event:${groupId}|mode=forward|cursor=after=${String(params.after_event_seq ?? 0)}|force=${String(Boolean(params.force))}|limit=${String(params.limit)}`;
561
583
  }
562
584
  return '';
563
585
  }
@@ -570,23 +592,63 @@ export class RpcPipeline {
570
592
  return true;
571
593
  }
572
594
  pullGateName(key) {
573
- if (key.startsWith('p2p:'))
574
- return 'p2p_message';
575
- if (key.startsWith('group:'))
576
- return 'group_message';
577
- if (key.startsWith('group_event:'))
578
- return 'group_event';
579
- return `other:${key}`;
595
+ void key;
596
+ return 'pull';
580
597
  }
581
598
  pullGateState(key) {
582
599
  const name = this.pullGateName(key);
583
600
  let state = this.pullGateStates.get(name);
584
601
  if (!state) {
585
- state = { active: null, foreground: [], background: [], byKey: new Map() };
602
+ state = { active: null, foreground: [], background: [], byKey: new Map(), lifecycle: new Map(), watchdog: null };
586
603
  this.pullGateStates.set(name, state);
587
604
  }
605
+ this.startPullGateWatchdog(state, name);
588
606
  return state;
589
607
  }
608
+ pullGateOperationTimeoutMs() {
609
+ const client = this.runtime.client;
610
+ const configured = Number(client._PULL_GATE_OPERATION_TIMEOUT_MS
611
+ ?? client._PULL_GATE_TIMEOUT_MS
612
+ ?? client._PULL_GATE_QUEUE_TIMEOUT_MS);
613
+ return Number.isFinite(configured) && configured > 0 ? configured : PULL_GATE_OPERATION_TIMEOUT_MS;
614
+ }
615
+ startPullGateWatchdog(state, name) {
616
+ if (state.watchdog)
617
+ return;
618
+ const interval = Math.min(50, Math.max(1, Math.floor(this.pullGateOperationTimeoutMs() / 4)));
619
+ const timer = setInterval(() => this.checkPullGate(state, name), interval);
620
+ state.watchdog = timer;
621
+ const unref = timer.unref;
622
+ unref?.call(timer);
623
+ }
624
+ stopPullGateWatchdogs() {
625
+ for (const state of this.pullGateStates.values()) {
626
+ if (state.watchdog === null)
627
+ continue;
628
+ clearInterval(state.watchdog);
629
+ state.watchdog = null;
630
+ }
631
+ }
632
+ checkPullGate(state, name) {
633
+ const active = state.active;
634
+ if (!active) {
635
+ this.drainPullGate(state);
636
+ return;
637
+ }
638
+ if (!active.pullingStartedAt || Date.now() - active.pullingStartedAt < this.pullGateOperationTimeoutMs())
639
+ return;
640
+ if (active.timedOut)
641
+ return;
642
+ active.timedOut = true;
643
+ active.invalidated = true;
644
+ this.pullGeneration += 1;
645
+ active.cancel?.();
646
+ const timeout = new TimeoutError(`pull gate timeout: ${active.namespace}`, { retryable: true });
647
+ this.runtime.client._clientLog.warn(`pull gate watchdog timeout: gate=${name} key=${active.key} elapsed=${Date.now() - active.pullingStartedAt}ms`);
648
+ // 仅拒绝当前调用方;取消不敏感的 operation 仍可能运行,必须等其
649
+ // 真正 settle 后才释放 active,避免与后续 Pull 并发修改游标。
650
+ active.reject(timeout);
651
+ }
590
652
  pullScopeKey(key) {
591
653
  return key.split('|', 1)[0] ?? key;
592
654
  }
@@ -594,37 +656,37 @@ export class RpcPipeline {
594
656
  const ns = String(namespace ?? '').trim();
595
657
  return Boolean(ns) && this.inlineRealtimeScopes.has(ns);
596
658
  }
597
- async executeBypassedPull(key, operation, background) {
598
- const scope = this.pullScopeKey(key);
599
- this.bypassedPullScopes.set(scope, (this.bypassedPullScopes.get(scope) ?? 0) + 1);
600
- try {
601
- return await this.executePullOperation(operation, background);
602
- }
603
- finally {
604
- const remaining = (this.bypassedPullScopes.get(scope) ?? 1) - 1;
605
- if (remaining <= 0) {
606
- this.bypassedPullScopes.delete(scope);
607
- }
608
- else {
609
- this.bypassedPullScopes.set(scope, remaining);
610
- }
611
- }
659
+ bindActivePullCancellation(method, params, request) {
660
+ const cancel = request.cancel;
661
+ const key = this.pullGateKeyForCall(method, params);
662
+ if (typeof cancel !== 'function' || !key)
663
+ return request;
664
+ const state = this.pullGateStates.get(this.pullGateName(key));
665
+ const active = state?.active;
666
+ if (!active || active.key !== key || active.timedOut || active.invalidated)
667
+ return request;
668
+ const boundCancel = () => cancel.call(request);
669
+ active.cancel = boundCancel;
670
+ return request.finally(() => {
671
+ if (active.cancel === boundCancel)
672
+ active.cancel = null;
673
+ });
612
674
  }
613
675
  isPullResponseProcessing(key) {
614
676
  if (!key)
615
677
  return false;
616
678
  return (this.runtime.client._pullResponseKeys.get(this.pullScopeKey(key)) ?? 0) > 0;
617
679
  }
618
- hasPullActivity(namespace) {
680
+ hasPullActivity(namespace, includePending = true) {
619
681
  const ns = String(namespace ?? '').trim();
620
682
  if ((!ns.startsWith('p2p:') && !ns.startsWith('group:')) || this.hasInlineRealtimeGuard(ns))
621
683
  return true;
622
684
  const client = this.runtime.client;
623
685
  if ((client._pullResponseKeys.get(ns) ?? 0) > 0)
624
686
  return true;
625
- if (Number(client._pendingP2pPullUpper?.get?.(ns) ?? 0) > 0)
687
+ if (includePending && Number(client._pendingP2pPullUpper?.get?.(ns) ?? 0) > 0)
626
688
  return true;
627
- if ((this.bypassedPullScopes.get(ns) ?? 0) > 0)
689
+ if (includePending && client._delivery?.hasPendingPull?.(ns))
628
690
  return true;
629
691
  for (const key of client._gapFillDone?.keys?.() ?? []) {
630
692
  const marker = String(key ?? '').trim();
@@ -660,6 +722,7 @@ export class RpcPipeline {
660
722
  const state = this.pullGateStates.get(this.pullGateName(ns));
661
723
  if (state) {
662
724
  this.drainPullGate(state);
725
+ this.runtime.client._delivery?.onPullGateIdle?.(ns);
663
726
  this.armQueuedPullTimers(state);
664
727
  }
665
728
  }
@@ -694,94 +757,188 @@ export class RpcPipeline {
694
757
  gate.inflight = false;
695
758
  gate.startedAt = 0;
696
759
  if (key.startsWith('p2p:')) {
697
- client._schedulePendingP2pPullIfNeeded(key, 'pull-gate-release');
760
+ client._schedulePendingP2pPullIfNeeded?.(key, 'pull-gate-release');
761
+ }
762
+ }
763
+ isPullGenerationCurrent(generation) {
764
+ return generation === this.pullGeneration;
765
+ }
766
+ invalidatePulls() {
767
+ if (this.pullInvalidationWait)
768
+ return this.pullInvalidationWait;
769
+ this.pullGeneration += 1;
770
+ const error = new Error('pull invalidated');
771
+ const waits = [];
772
+ for (const state of this.pullGateStates.values()) {
773
+ const jobs = new Set();
774
+ if (state.active)
775
+ jobs.add(state.active);
776
+ for (const job of state.foreground)
777
+ jobs.add(job);
778
+ for (const job of state.background)
779
+ jobs.add(job);
780
+ for (const job of jobs) {
781
+ job.invalidated = true;
782
+ if (!job.timedOut)
783
+ job.reject(error);
784
+ job.cancel?.();
785
+ job.resumeResolve?.();
786
+ job.resumeResolve = null;
787
+ if (job.running) {
788
+ waits.push(job.settled);
789
+ }
790
+ else {
791
+ job.settledResolve?.();
792
+ job.settledResolve = null;
793
+ }
794
+ }
795
+ state.foreground = state.foreground.filter((job) => !jobs.has(job));
796
+ state.background = state.background.filter((job) => !jobs.has(job));
797
+ for (const [key, job] of state.byKey.entries()) {
798
+ if (jobs.has(job) && !job.running)
799
+ state.byKey.delete(key);
800
+ }
801
+ this.drainPullGate(state);
698
802
  }
803
+ const wait = Promise.all(waits).then(() => undefined);
804
+ let tracked;
805
+ tracked = wait.finally(() => {
806
+ if (this.pullInvalidationWait === tracked)
807
+ this.pullInvalidationWait = null;
808
+ });
809
+ this.pullInvalidationWait = tracked;
810
+ return tracked;
811
+ }
812
+ pullInvalidationInProgress() {
813
+ return this.pullInvalidationWait !== null;
699
814
  }
700
815
  async runPullSerialized(key, operation, background = false) {
701
- if (key && this.isPullResponseProcessing(key)) {
702
- this.runtime.client._clientLog.debug(`pull skipped while processing pull response: key=${key}`);
703
- return [];
816
+ if (this.pullInvalidationInProgress()) {
817
+ throw new Error('pull invalidated');
704
818
  }
705
819
  if (!key)
706
820
  return await this.executePullOperation(operation, background);
707
821
  const state = this.pullGateState(key);
708
- const timeoutMs = Math.max(0, Number(this.runtime.client._PULL_GATE_QUEUE_TIMEOUT_MS ?? 3_000) || 0);
709
- const existing = state.byKey.get(key);
822
+ const namespace = this.pullScopeKey(key);
823
+ const active = state.active?.key === key && !state.active.timedOut && !state.active.invalidated ? state.active : null;
824
+ const queuedForeground = state.foreground.find((job) => job.key === key && !job.timedOut && !job.invalidated);
825
+ const queuedBackground = state.background.find((job) => job.key === key && !job.timedOut && !job.invalidated);
826
+ const existing = background
827
+ ? (active ?? queuedForeground ?? queuedBackground)
828
+ : (queuedForeground ?? (active && !active.background ? active : null) ?? queuedBackground);
710
829
  if (existing) {
830
+ if (!background && existing.background && existing !== state.active) {
831
+ const index = state.background.indexOf(existing);
832
+ if (index >= 0)
833
+ state.background.splice(index, 1);
834
+ existing.background = false;
835
+ state.foreground.push(existing);
836
+ }
837
+ const shared = existing.promise;
711
838
  this.runtime.client._clientLog.debug(`pull gate coalesced: gate=${this.pullGateName(key)} key=${key} background=${String(background)}`);
712
- if (this.hasInlineRealtimeGuard(this.pullScopeKey(key)))
713
- return await existing.promise;
714
- return await this.waitForCoalesced(existing.promise, timeoutMs, operation, background, key);
839
+ return await shared;
715
840
  }
716
841
  let resolve;
717
842
  let reject;
718
843
  const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
844
+ let settledResolve;
845
+ const settled = new Promise((resolveSettled) => { settledResolve = resolveSettled; });
846
+ void promise.catch(() => { });
719
847
  const job = {
720
848
  key,
849
+ namespace,
721
850
  background,
851
+ running: false,
852
+ suspended: false,
722
853
  enqueuedAt: Date.now(),
723
854
  operation,
724
855
  promise,
725
856
  resolve,
726
857
  reject,
727
858
  timer: null,
859
+ resume: null,
860
+ resumeResolve: null,
861
+ pullingStartedAt: 0,
862
+ timedOut: false,
863
+ invalidated: false,
864
+ cancel: null,
865
+ settled,
866
+ settledResolve,
728
867
  };
729
868
  const queue = background ? state.background : state.foreground;
730
869
  queue.push(job);
731
870
  state.byKey.set(key, job);
732
871
  this.drainPullGate(state);
733
- if (state.active !== job && !this.hasInlineRealtimeGuard(this.pullScopeKey(job.key))) {
734
- job.timer = setTimeout(() => this.bypassQueuedPull(state, job), timeoutMs);
735
- }
872
+ this.checkPullGate(state, this.pullGateName(key));
736
873
  return await promise;
737
874
  }
738
875
  async executePullOperation(operation, background) {
739
- if (background)
740
- return await this.runtime.client._withBackgroundRpc(operation);
741
- return await operation();
742
- }
743
- waitForCoalesced(shared, timeoutMs, operation, background, key) {
744
- return new Promise((resolve, reject) => {
745
- let settled = false;
746
- const timer = setTimeout(() => {
747
- if (settled)
748
- return;
749
- settled = true;
750
- this.runtime.client._clientLog.warn(`pull gate coalesced wait timeout, bypass: gate=${this.pullGateName(key)} key=${key} timeout=${timeoutMs}ms`);
751
- void this.executeBypassedPull(key, operation, background).then(resolve, reject);
752
- }, timeoutMs);
753
- shared.then((value) => {
754
- if (settled)
755
- return;
756
- settled = true;
757
- clearTimeout(timer);
758
- resolve(value);
759
- }, (error) => {
760
- if (settled)
761
- return;
762
- settled = true;
763
- clearTimeout(timer);
764
- reject(error);
765
- });
766
- });
876
+ const client = this.runtime.client;
877
+ const hadPrevious = Object.prototype.hasOwnProperty.call(client, '_pullOperationGeneration');
878
+ const previous = client._pullOperationGeneration;
879
+ client._pullOperationGeneration = this.pullGeneration;
880
+ try {
881
+ if (background)
882
+ return await client._withBackgroundRpc(operation);
883
+ return await operation();
884
+ }
885
+ finally {
886
+ if (hadPrevious)
887
+ client._pullOperationGeneration = previous;
888
+ else
889
+ delete client._pullOperationGeneration;
890
+ }
767
891
  }
768
- bypassQueuedPull(state, job) {
769
- if (state.active === job)
770
- return;
771
- if (this.hasInlineRealtimeGuard(this.pullScopeKey(job.key))) {
772
- job.timer = null;
892
+ pullOperationIsCurrent() {
893
+ const generation = this.runtime.client._pullOperationGeneration;
894
+ return generation === undefined || this.isPullGenerationCurrent(generation);
895
+ }
896
+ throwIfPullInvalidated() {
897
+ if (!this.pullOperationIsCurrent())
898
+ throw new Error('pull invalidated');
899
+ }
900
+ async yieldPullGate(key, nextKey, background) {
901
+ if (!key)
773
902
  return;
774
- }
775
- const queue = job.background ? state.background : state.foreground;
776
- const index = queue.indexOf(job);
777
- if (index < 0)
903
+ const state = this.pullGateState(key);
904
+ const namespace = this.pullScopeKey(key);
905
+ const job = state.active?.key === key ? state.active : null;
906
+ if (!job || state.active !== job || job.timedOut || job.invalidated)
778
907
  return;
779
- queue.splice(index, 1);
908
+ const replacementKey = String(nextKey || key);
780
909
  if (state.byKey.get(job.key) === job)
781
910
  state.byKey.delete(job.key);
782
- job.timer = null;
783
- this.runtime.client._clientLog.warn(`pull gate queue timeout, bypass: gate=${this.pullGateName(job.key)} key=${job.key} background=${String(job.background)}`);
784
- void this.executeBypassedPull(job.key, job.operation, job.background).then(job.resolve, job.reject);
911
+ job.key = replacementKey;
912
+ job.namespace = this.pullScopeKey(replacementKey);
913
+ if (background !== undefined)
914
+ job.background = background;
915
+ state.byKey.set(job.key, job);
916
+ state.lifecycle.set(namespace, 'pulled');
917
+ job.suspended = true;
918
+ job.resume = new Promise((resolve) => { job.resumeResolve = resolve; });
919
+ state.active = null;
920
+ (job.background ? state.background : state.foreground).push(job);
921
+ this.drainPullGate(state);
922
+ if (job.resume) {
923
+ await job.resume;
924
+ if (job.invalidated)
925
+ throw new Error('pull invalidated');
926
+ }
927
+ }
928
+ getPullLifecycle(namespace) {
929
+ const ns = this.pullScopeKey(namespace);
930
+ if (!ns)
931
+ return 'idle';
932
+ return this.pullGateStates.get(this.pullGateName(ns))?.lifecycle.get(ns) ?? 'idle';
933
+ }
934
+ setPullLifecycle(namespace, lifecycle) {
935
+ const ns = this.pullScopeKey(namespace);
936
+ if (!ns)
937
+ return;
938
+ const state = this.pullGateStates.get(this.pullGateName(ns));
939
+ if (!state || state.active?.namespace !== ns)
940
+ return;
941
+ state.lifecycle.set(ns, lifecycle);
785
942
  }
786
943
  drainPullGate(state) {
787
944
  if (state.active)
@@ -798,24 +955,37 @@ export class RpcPipeline {
798
955
  clearTimeout(job.timer);
799
956
  job.timer = null;
800
957
  }
801
- void this.executePullOperation(job.operation, job.background)
802
- .then(job.resolve, job.reject)
958
+ job.pullingStartedAt = Date.now();
959
+ job.timedOut = false;
960
+ state.lifecycle.set(job.namespace, 'pulling');
961
+ const resumeResolve = job.resumeResolve;
962
+ job.resume = null;
963
+ job.resumeResolve = null;
964
+ if (job.suspended) {
965
+ job.suspended = false;
966
+ resumeResolve?.();
967
+ return;
968
+ }
969
+ job.running = true;
970
+ const operationPromise = this.executePullOperation(job.operation, job.background);
971
+ void operationPromise
972
+ .then((value) => { if (!job.timedOut && !job.invalidated)
973
+ job.resolve(value); }, (error) => { if (!job.timedOut && !job.invalidated)
974
+ job.reject(error); })
803
975
  .finally(() => {
976
+ state.lifecycle.set(job.namespace, 'idle');
804
977
  if (state.active === job)
805
978
  state.active = null;
806
979
  if (state.byKey.get(job.key) === job)
807
980
  state.byKey.delete(job.key);
808
981
  this.drainPullGate(state);
982
+ job.settledResolve?.();
983
+ job.settledResolve = null;
984
+ if (!job.invalidated)
985
+ this.runtime.client._delivery?.onPullGateIdle?.(job.namespace);
809
986
  });
810
987
  }
811
- armQueuedPullTimers(state) {
812
- const timeoutMs = Math.max(0, Number(this.runtime.client._PULL_GATE_QUEUE_TIMEOUT_MS ?? 3_000) || 0);
813
- for (const job of [...state.foreground, ...state.background]) {
814
- if (!job.timer && !this.hasInlineRealtimeGuard(this.pullScopeKey(job.key))) {
815
- job.timer = setTimeout(() => this.bypassQueuedPull(state, job), timeoutMs);
816
- }
817
- }
818
- }
988
+ armQueuedPullTimers(_state) { }
819
989
  // ── raw-call ───────────────────────────────────────────────────────────────
820
990
  async rawCall(method, params, opts = {}) {
821
991
  const { timeout, trace, signed = true, background = false } = opts;
@@ -824,20 +994,50 @@ export class RpcPipeline {
824
994
  if (signed) {
825
995
  this.applyClientSignature(method, payload);
826
996
  }
827
- if (background) {
828
- return await client._transport.call(method, payload, timeout, trace, true);
829
- }
830
- if (trace !== undefined) {
831
- return await client._transport.call(method, payload, timeout, trace);
997
+ const invokeTransport = () => {
998
+ let request;
999
+ if (background)
1000
+ request = client._transport.call(method, payload, timeout, trace, true);
1001
+ else if (trace !== undefined)
1002
+ request = client._transport.call(method, payload, timeout, trace);
1003
+ else if (timeout !== undefined)
1004
+ request = client._transport.call(method, payload, timeout);
1005
+ else
1006
+ request = client._transport.call(method, payload);
1007
+ return this.bindActivePullCancellation(method, payload, request);
1008
+ };
1009
+ const result = await this.transportCallWithIdentityRecovery(invokeTransport, method, payload);
1010
+ this.throwIfPullInvalidated();
1011
+ return result;
1012
+ }
1013
+ async transportCallWithIdentityRecovery(operation, method, params) {
1014
+ try {
1015
+ return await operation();
832
1016
  }
833
- if (timeout !== undefined) {
834
- return await client._transport.call(method, payload, timeout);
1017
+ catch (err) {
1018
+ const recover = this.runtime.client._recoverIdentityAdmission;
1019
+ if (typeof recover !== 'function' || !await recover.call(this.runtime.client, err, method, params))
1020
+ throw err;
1021
+ let lastError = err;
1022
+ for (const delay of IDENTITY_ADMISSION_RETRY_DELAYS_MS) {
1023
+ if (delay > 0)
1024
+ await new Promise((resolve) => setTimeout(resolve, delay));
1025
+ try {
1026
+ return await operation();
1027
+ }
1028
+ catch (retryError) {
1029
+ if (!sameIdentityAdmissionRejection(retryError, err))
1030
+ throw retryError;
1031
+ lastError = retryError;
1032
+ }
1033
+ }
1034
+ throw lastError;
835
1035
  }
836
- return await client._transport.call(method, payload);
837
1036
  }
838
1037
  // ── postprocess ────────────────────────────────────────────────────────────
839
- async postprocessResult(method, params, result) {
1038
+ async postprocessResult(method, params, result, options = {}) {
840
1039
  const client = this.runtime.client;
1040
+ this.throwIfPullInvalidated();
841
1041
  let next = result;
842
1042
  if ((method === 'group.send' || method === 'group.v2.send') && isUnknownJsonObject(next)) {
843
1043
  next = normalizeGroupMentionMode(next);
@@ -854,12 +1054,40 @@ export class RpcPipeline {
854
1054
  next = await client._decryptMessageThoughts(next);
855
1055
  }
856
1056
  if (method === 'message.pull' && isUnknownJsonObject(next)) {
857
- this.postprocessMessagePull(params, next);
1057
+ const isTail = String(next.window_mode ?? params.window_mode ?? '').trim().toLowerCase() === 'tail';
1058
+ if (!isTail && client._aid && client._seqTracker) {
1059
+ const ns = `p2p:${client._aid}`;
1060
+ const afterSeq = Math.max(0, Number(params.after_seq ?? 0) || 0);
1061
+ await client._delivery.commitPlainForwardPage(ns, afterSeq, 'message.pull', next);
1062
+ }
1063
+ else {
1064
+ this.postprocessMessagePull(params, next);
1065
+ }
858
1066
  }
859
1067
  if (method === 'group.pull' && isUnknownJsonObject(next)) {
860
- this.postprocessGroupPull(params, next);
1068
+ if (Array.isArray(next.messages)) {
1069
+ next.messages = next.messages.map((message) => normalizeGroupMentionMode(message));
1070
+ }
1071
+ const gid = String(params.group_id ?? params.group_aid ?? '').trim();
1072
+ const explicitCursorParams = this.explicitGroupCursorParams(params);
1073
+ const ownsCursor = Object.keys(explicitCursorParams).length === 0
1074
+ || this.groupCursorTargetsCurrentInstance(explicitCursorParams);
1075
+ const isTail = String(next.window_mode ?? params.window_mode ?? '').trim().toLowerCase() === 'tail';
1076
+ if (!ownsCursor) {
1077
+ client._clientLog.debug(`group.pull external cursor skips local Forward commit: group=${gid}`);
1078
+ }
1079
+ else if (!isTail && gid && client._seqTracker) {
1080
+ const ns = `group:${gid}`;
1081
+ const afterSeq = Math.max(0, Number(params.after_message_seq ?? params.after_seq ?? 0) || 0);
1082
+ await client._delivery.commitPlainForwardPage(ns, afterSeq, 'group.pull', next, gid);
1083
+ }
1084
+ else {
1085
+ this.postprocessGroupPull(params, next);
1086
+ }
1087
+ }
1088
+ if (!options.skipGroupState) {
1089
+ next = await client._groupState.postprocessResult(method, params, next);
861
1090
  }
862
- next = await client._groupState.postprocessResult(method, params, next);
863
1091
  return next;
864
1092
  }
865
1093
  // ── private helpers ────────────────────────────────────────────────────────
@@ -973,6 +1201,19 @@ export class RpcPipeline {
973
1201
  }
974
1202
  return cursorParams;
975
1203
  }
1204
+ explicitGroupCursorParams(params) {
1205
+ const value = params._group_cursor_params;
1206
+ if (!isJsonObject(value))
1207
+ return {};
1208
+ return { ...value };
1209
+ }
1210
+ groupCursorTargetsCurrentInstance(params) {
1211
+ const client = this.runtime.client;
1212
+ const deviceId = String(params.device_id ?? '').trim();
1213
+ const slotId = String(params.slot_id ?? '').trim();
1214
+ return (!deviceId || deviceId === (client._deviceId ?? ''))
1215
+ && (!slotId || slotId === (client._slotId ?? ''));
1216
+ }
976
1217
  validateMessageRecipient(toAid) {
977
1218
  if (!String(toAid ?? '').trim()) {
978
1219
  throw new ValidationError("message.send requires 'to' (recipient AID)");