@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
package/dist/client.js CHANGED
@@ -16,17 +16,17 @@ import * as https from 'node:https';
16
16
  import { URL } from 'node:url';
17
17
  import { configFromMap, getDeviceId, normalizeSlotId } from './config.js';
18
18
  import { CryptoProvider } from './crypto.js';
19
- import { AIDStore } from './aid-store.js';
20
19
  import { GatewayDiscovery } from './discovery.js';
21
20
  import { DnsResilientNet } from './net.js';
22
21
  import { AUNError, AuthError, ConnectionError, E2EEError, NotFoundError, PermissionError, StateError, TimeoutError, ValidationError, } from './errors.js';
23
22
  import { EventDispatcher } from './events.js';
24
23
  import { LocalTokenStore } from './keystore/local-token-store.js';
25
24
  import { AUNLogger } from './logger.js';
25
+ import { normalizeGroupId } from './group-id.js';
26
26
  import { validateAIDFormat, validateGroupIDFormat } from './validators.js';
27
27
  import { RPCTransport } from './transport.js';
28
28
  import { AuthFlow } from './auth.js';
29
- import { AgentMdManager } from './agent-md.js';
29
+ import { AgentMdManager, buildDefaultAgentMd } from './agent-md.js';
30
30
  import { GroupIndexMetaCache } from './group-index.js';
31
31
  import { SeqTracker } from './seq-tracker.js';
32
32
  import { ClientRuntime } from './client/runtime.js';
@@ -37,7 +37,7 @@ import { LifecycleController } from './client/lifecycle.js';
37
37
  import { PeerDirectory } from './client/peers.js';
38
38
  import { RpcPipeline } from './client/rpc-pipeline.js';
39
39
  import { CollabClient } from './collab/client.js';
40
- import { GroupFacade, MessageFacade, StreamFacade } from './facades.js';
40
+ import { GroupFacade, MailFacade, MessageFacade, StreamFacade } from './facades.js';
41
41
  import { StorageVFS } from './storage/vfs.js';
42
42
  import { V2E2EECoordinator } from './client/v2-e2ee.js';
43
43
  import { GroupStateCoordinator } from './client/group-state.js';
@@ -45,7 +45,7 @@ import { decryptMessage, } from './v2/e2ee/index.js';
45
45
  import { ecdsaVerifyRaw } from './v2/crypto/ecdsa.js';
46
46
  import { isJsonObject, ConnectionState, STATE_TO_PUBLIC, } from './types.js';
47
47
  import { AID } from './aid.js';
48
- import { certMatchesFingerprint, normalizeFingerprintHex } from './cert-utils.js';
48
+ import { certMatchesFingerprint, normalizeFingerprintHex, publicKeyMatchesFingerprint } from './cert-utils.js';
49
49
  function isPromiseLike(value) {
50
50
  return Boolean(value && typeof value.then === 'function');
51
51
  }
@@ -124,7 +124,7 @@ const MAX_NOTIFY_PAYLOAD_SIZE = 64 * 1024;
124
124
  // P1-23: 非幂等方法使用更长超时(35s),避免 SDK 10s 超时 < gateway 30s 处理时间
125
125
  const NON_IDEMPOTENT_TIMEOUT_MS = 35_000;
126
126
  const NON_IDEMPOTENT_METHODS = new Set([
127
- 'message.send', 'group.send', 'group.create', 'group.invite',
127
+ 'message.send', 'mail.send', 'group.send', 'group.create', 'group.invite',
128
128
  'group.kick', 'group.remove_member', 'group.leave', 'group.dissolve',
129
129
  'group.set_settings',
130
130
  'group.update_announcement', 'group.update_rules',
@@ -332,6 +332,7 @@ function createAgentMdManagerForRuntime(opts) {
332
332
  aunPath: opts.config().aunPath,
333
333
  verifySsl: opts.config().verifySsl,
334
334
  discoveryPort: opts.config().discoveryPort,
335
+ uploadViaGatewayHost: true,
335
336
  logger: opts.logger(),
336
337
  ownerAidGetter: opts.ownerAid,
337
338
  currentAidGetter: opts.currentAid,
@@ -435,6 +436,7 @@ export class AUNClient {
435
436
  _gatewayCandidates = [];
436
437
  /** 是否正在关闭 */
437
438
  _closing = false;
439
+ _disconnecting = false;
438
440
  /** 事件调度器 */
439
441
  _dispatcher;
440
442
  /** Gateway 发现 */
@@ -458,6 +460,8 @@ export class AUNClient {
458
460
  /** peer 证书缓存 */
459
461
  _certCache = new Map();
460
462
  _agentMdManager;
463
+ _aidStore;
464
+ _groupIdentityOperations = new Map();
461
465
  _groupIndexMetaCache = new GroupIndexMetaCache();
462
466
  /** 消息序列号跟踪器(群消息 + P2P 空洞检测) */
463
467
  _seqTracker = new SeqTracker();
@@ -483,12 +487,6 @@ export class AUNClient {
483
487
  _pushProcessQueues = new Map();
484
488
  /** P2P pull 进行中到达的纯通知 push 上界;pull gate 释放后需要补拉一次。 */
485
489
  _pendingP2pPullUpper = new Map();
486
- /** 在线未读 hint 队列:同一 group 只保留最后一条,延迟 drain 降低登录瞬时拉取压力。 */
487
- _onlineUnreadHintQueue = new Map();
488
- _onlineUnreadHintTimer = null;
489
- _onlineUnreadHintDrainActive = false;
490
- _onlineUnreadHintInitialDelayMs = 750;
491
- _onlineUnreadHintIntervalMs = 50;
492
490
  /** 缺 sender IK 时暂存原始 V2 消息,后台补齐 IK 后重试解密。 */
493
491
  _v2SenderIKPending = new Map();
494
492
  /** sender IK 后台补齐任务去重。 */
@@ -520,6 +518,8 @@ export class AUNClient {
520
518
  _peerCache = new Map();
521
519
  _reconnectActive = false;
522
520
  _reconnectAbort = null;
521
+ _reconnectTask = null;
522
+ _reconnectEventDispatchDepth = 0;
523
523
  _serverKicked = false;
524
524
  /** 缓存最近一次 gateway.disconnect 信息(含服务端附带的 detail),用于后续 connection.state 透传 */
525
525
  _lastDisconnectInfo = null;
@@ -536,6 +536,7 @@ export class AUNClient {
536
536
  _storage;
537
537
  _collab;
538
538
  _message;
539
+ _mail;
539
540
  _group;
540
541
  _stream;
541
542
  constructor(aid) {
@@ -543,6 +544,7 @@ export class AUNClient {
543
544
  throw new ValidationError('AUNClient only accepts an AID object or no argument');
544
545
  }
545
546
  const inputAid = aid ?? null;
547
+ this._aidStore = inputAid?._aidStore;
546
548
  const rawConfig = {};
547
549
  if (inputAid) {
548
550
  rawConfig.aun_path = inputAid.aunPath;
@@ -685,7 +687,7 @@ export class AUNClient {
685
687
  // 群组状态提交事件:验证 state_hash 链并更新本地存储
686
688
  this._dispatcher.subscribe('_raw.group.state_committed', (data) => this._onGroupStateCommitted(data));
687
689
  // 其他事件直接透传
688
- for (const evt of ['message.ack', 'storage.object_changed']) {
690
+ for (const evt of ['message.ack', 'storage.object_changed', 'stream/created', 'stream/closed']) {
689
691
  this._dispatcher.subscribe(`_raw.${evt}`, (data) => this._dispatcher.publish(evt, data));
690
692
  }
691
693
  // 服务端主动断开通知:记录日志并标记不重连
@@ -696,6 +698,14 @@ export class AUNClient {
696
698
  get aid() {
697
699
  return this._aid;
698
700
  }
701
+ /** 返回后台刷新任务维护的最新 access token。 */
702
+ getAccessToken() {
703
+ const identityToken = String(this._identity?.access_token ?? '').trim();
704
+ if (identityToken)
705
+ return identityToken;
706
+ const sessionToken = String(this._sessionParams?.access_token ?? '').trim();
707
+ return sessionToken || null;
708
+ }
699
709
  /** AUN Storage VFS 入口 */
700
710
  get storage() {
701
711
  if (!this._storage)
@@ -804,6 +814,7 @@ export class AUNClient {
804
814
  this._v2LazyProposeTriggered.clear();
805
815
  }
806
816
  _applyAidRuntimeContext(aid) {
817
+ this._aidStore = aid._aidStore;
807
818
  const oldTransport = this._transport;
808
819
  try {
809
820
  const closeResult = oldTransport?.close?.();
@@ -841,6 +852,7 @@ export class AUNClient {
841
852
  this._peerCache.clear();
842
853
  this._certCache.clear();
843
854
  this._gatewayUrl = null;
855
+ this._gatewayCandidates = [];
844
856
  this._deviceId = aid.deviceId || getDeviceId(nextConfig.aunPath);
845
857
  this._slotId = aid.slotId || 'default';
846
858
  const debugFlag = nextConfig.debug;
@@ -1011,16 +1023,124 @@ export class AUNClient {
1011
1023
  return;
1012
1024
  await this._handleTransportDisconnect(err);
1013
1025
  }
1014
- async createGroup(params = {}) {
1026
+ async _recoverIdentityAdmission(err, _method = '', params = {}) {
1027
+ if (!(err instanceof AUNError) || err.code !== -32003)
1028
+ return false;
1029
+ const message = String(err.message ?? '').trim();
1030
+ const groupCertPrefix = 'valid group identity certificate is required for ';
1031
+ if (message.toLowerCase().startsWith(groupCertPrefix)) {
1032
+ const fallback = message.slice(groupCertPrefix.length).trim();
1033
+ const store = this._aidStore;
1034
+ const groupId = String(params.group_id ?? params.groupId ?? params.group_aid ?? params.groupAid ?? fallback).trim();
1035
+ if (!groupId || !store)
1036
+ return false;
1037
+ try {
1038
+ await this._runGroupIdentityOperation(groupId, () => this.bindGroupAid({ group_id: groupId }, { aidStore: store }));
1039
+ }
1040
+ catch {
1041
+ return false;
1042
+ }
1043
+ return true;
1044
+ }
1045
+ const groupMDPrefix = 'valid group agent.md is required for ';
1046
+ if (message.toLowerCase().startsWith(groupMDPrefix)) {
1047
+ const groupAid = message.slice(groupMDPrefix.length).trim();
1048
+ const store = this._aidStore;
1049
+ if (!groupAid || !store)
1050
+ return false;
1051
+ const loaded = store.load(groupAid);
1052
+ if (!loaded.ok || !loaded.data?.aid?.isPrivateKeyValid())
1053
+ return false;
1054
+ await this._runGroupIdentityOperation(groupAid, () => this._uploadGroupAgentMd(store, groupAid, {}, {}));
1055
+ return true;
1056
+ }
1057
+ const agentPrefix = 'valid agent.md is required for ';
1058
+ if (!message.toLowerCase().startsWith(agentPrefix))
1059
+ return false;
1060
+ const target = message.slice(agentPrefix.length).trim();
1061
+ if (!target || target.toLowerCase() !== String(this._aid ?? '').trim().toLowerCase())
1062
+ return false;
1063
+ let agentMd = buildDefaultAgentMd(target);
1064
+ try {
1065
+ agentMd = this._agentMdManager.readContent(target);
1066
+ }
1067
+ catch { }
1068
+ await this._agentMdManager.upload(agentMd);
1069
+ return true;
1070
+ }
1071
+ async _runGroupIdentityOperation(groupId, operation) {
1072
+ const aid = String(this._aid ?? '').trim();
1073
+ const dot = aid.indexOf('.');
1074
+ const key = normalizeGroupId(groupId, { localIssuer: dot >= 0 ? aid.slice(dot + 1) : '' }).toLowerCase();
1075
+ if (!key)
1076
+ return operation();
1077
+ let task = this._groupIdentityOperations.get(key);
1078
+ if (!task) {
1079
+ task = Promise.resolve().then(operation);
1080
+ this._groupIdentityOperations.set(key, task);
1081
+ }
1082
+ try {
1083
+ return await task;
1084
+ }
1085
+ finally {
1086
+ if (this._groupIdentityOperations.get(key) === task)
1087
+ this._groupIdentityOperations.delete(key);
1088
+ }
1089
+ }
1090
+ async createGroup(params = {}, options = {}) {
1015
1091
  if (!isJsonObject(params)) {
1016
1092
  throw new ValidationError('createGroup params must be an object');
1017
1093
  }
1094
+ const store = options.aidStore;
1095
+ if (!store) {
1096
+ throw new ValidationError('createGroup requires aidStore');
1097
+ }
1098
+ this._aidStore = store;
1018
1099
  const payload = { ...params };
1019
- const groupName = String(payload.group_name ?? payload.groupName ?? '').trim();
1020
- if (!groupName) {
1021
- return await this.call('group.create', payload);
1100
+ if (payload.group_name === undefined && payload.groupName !== undefined) {
1101
+ payload.group_name = payload.groupName;
1102
+ delete payload.groupName;
1103
+ }
1104
+ const isNamed = Boolean(String(payload.group_name ?? '').trim());
1105
+ payload._defer_group_ready_postprocess = true;
1106
+ if (!isNamed) {
1107
+ delete payload.public_key;
1108
+ delete payload.curve;
1109
+ const created = await this.call('group.create', payload);
1110
+ const createdMap = isJsonObject(created) ? created : {};
1111
+ const createdGroup = isJsonObject(createdMap.group) ? createdMap.group : {};
1112
+ const groupId = String(createdGroup.group_id ?? createdGroup.group_aid ?? createdMap.group_id ?? createdMap.group_aid ?? '').trim();
1113
+ if (!groupId) {
1114
+ throw new ValidationError('group.create response missing group.group_id');
1115
+ }
1116
+ const bindParams = { group_id: groupId };
1117
+ for (const key of ['name', 'description', 'group_description', 'group_agent_md', 'groupAgentMd', 'content']) {
1118
+ if (key in payload)
1119
+ bindParams[key] = payload[key];
1120
+ }
1121
+ const bound = await this._runGroupIdentityOperation(groupId, () => this.bindGroupAid(bindParams, { aidStore: store }));
1122
+ const boundMap = isJsonObject(bound) ? bound : {};
1123
+ const boundGroup = isJsonObject(boundMap.group) ? boundMap.group : {};
1124
+ const result = { ...createdMap, ...boundMap, group: { ...createdGroup, ...boundGroup } };
1125
+ const postprocessParams = { ...payload };
1126
+ delete postprocessParams._defer_group_ready_postprocess;
1127
+ return await this._groupState.postprocessResult('group.create', postprocessParams, result);
1128
+ }
1129
+ const pendingKey = `create:${String(payload.group_name).trim().toLowerCase()}`;
1130
+ const keystore = store._keystore;
1131
+ let keyPair = null;
1132
+ if (keystore && typeof keystore.loadPendingGroupBind === 'function') {
1133
+ keyPair = keystore.loadPendingGroupBind(pendingKey);
1134
+ }
1135
+ if (!keyPair || !keyPair.public_key_der_b64 || !keyPair.private_key_pem) {
1136
+ keyPair = new CryptoProvider().generateIdentity();
1137
+ if (keystore && typeof keystore.savePendingGroupBind === 'function') {
1138
+ keystore.savePendingGroupBind(pendingKey, keyPair);
1139
+ }
1140
+ }
1141
+ if (!keyPair || !keyPair.public_key_der_b64 || !keyPair.private_key_pem) {
1142
+ throw new ValidationError('createGroup: failed to generate or load key pair');
1022
1143
  }
1023
- const keyPair = new CryptoProvider().generateIdentity();
1024
1144
  payload.public_key = keyPair.public_key_der_b64;
1025
1145
  payload.curve = keyPair.curve;
1026
1146
  const result = await this.call('group.create', payload);
@@ -1030,31 +1150,65 @@ export class AUNClient {
1030
1150
  const groupAid = String(group.group_aid ?? resultMap.group_aid ?? '').trim();
1031
1151
  const certPem = String(aidCert.cert ?? aidCert.cert_pem ?? resultMap.cert ?? resultMap.cert_pem ?? '').trim();
1032
1152
  if (!groupAid || !certPem) {
1033
- throw new ValidationError('group.create named group response missing group.group_aid or aid_cert.cert');
1153
+ throw new ValidationError('group.create response missing group.group_aid or aid_cert.cert');
1034
1154
  }
1035
- const store = new AIDStore({
1036
- aunPath: this._configModel.aunPath,
1037
- encryptionSeed: this._configModel.seedPassword ?? '',
1038
- slotId: this._slotId,
1039
- verifySsl: this._configModel.verifySsl,
1040
- rootCaPath: this._configModel.rootCaPath,
1041
- debug: this._configModel.debug,
1155
+ const imported = store.importGroupIdentity(groupAid, {
1156
+ private_key_pem: keyPair.private_key_pem,
1157
+ public_key_der_b64: keyPair.public_key_der_b64,
1158
+ curve: keyPair.curve,
1159
+ cert_pem: certPem,
1042
1160
  });
1043
- try {
1044
- const imported = store.importGroupIdentity(groupAid, {
1045
- private_key_pem: keyPair.private_key_pem,
1046
- public_key_der_b64: keyPair.public_key_der_b64,
1047
- curve: keyPair.curve,
1048
- cert_pem: certPem,
1049
- });
1050
- if (!imported.ok) {
1051
- throw new ValidationError(imported.error.message);
1052
- }
1161
+ if (!imported.ok) {
1162
+ throw new ValidationError(imported.error.message);
1053
1163
  }
1054
- finally {
1055
- store.close();
1164
+ await this._uploadGroupAgentMd(store, groupAid, payload, group);
1165
+ if (keystore && typeof keystore.clearPendingGroupBind === 'function') {
1166
+ keystore.clearPendingGroupBind(pendingKey);
1167
+ }
1168
+ const postprocessParams = { ...payload };
1169
+ delete postprocessParams._defer_group_ready_postprocess;
1170
+ return await this._groupState.postprocessResult('group.create', postprocessParams, result);
1171
+ }
1172
+ _groupAgentMdContent(groupAid, params, group) {
1173
+ const explicit = params.group_agent_md ?? params.groupAgentMd ?? params.content;
1174
+ if (explicit !== undefined && explicit !== null)
1175
+ return String(explicit);
1176
+ const value = (...keys) => {
1177
+ for (const source of [group, params]) {
1178
+ for (const key of keys) {
1179
+ const item = String(source[key] ?? '').trim();
1180
+ if (item)
1181
+ return item;
1182
+ }
1183
+ }
1184
+ return '';
1185
+ };
1186
+ const name = value('name', 'group_name', 'title') || groupAid;
1187
+ const description = (value('description', 'group_description') || `AUN group ${name}`)
1188
+ .replace(/\s+/g, ' ')
1189
+ .slice(0, 100);
1190
+ return [
1191
+ '---',
1192
+ `aid: ${JSON.stringify(groupAid)}`,
1193
+ `name: ${JSON.stringify(name)}`,
1194
+ 'type: "group"',
1195
+ 'version: "1.0.0"',
1196
+ `description: ${JSON.stringify(description)}`,
1197
+ '---',
1198
+ '',
1199
+ `# ${name}`,
1200
+ '',
1201
+ ].join('\n');
1202
+ }
1203
+ async _uploadGroupAgentMd(store, groupAid, params, group) {
1204
+ const uploaderAid = String(this._aid ?? '').trim();
1205
+ if (!uploaderAid) {
1206
+ throw new ValidationError('group agent.md upload requires current owner AID');
1207
+ }
1208
+ const uploaded = await store.uploadAgentMd(groupAid, this._groupAgentMdContent(groupAid, params, group), { uploaderAid });
1209
+ if (!uploaded.ok) {
1210
+ throw new ValidationError(`group agent.md upload failed: ${uploaded.error.message}`);
1056
1211
  }
1057
- return result;
1058
1212
  }
1059
1213
  async bindGroupAid(params = {}, options = {}) {
1060
1214
  if (!isJsonObject(params)) {
@@ -1064,12 +1218,17 @@ export class AUNClient {
1064
1218
  if (!store) {
1065
1219
  throw new ValidationError('bindGroupAid requires aidStore');
1066
1220
  }
1221
+ this._aidStore = store;
1067
1222
  const groupId = String(params.group_id ?? params.group_aid ?? '').trim();
1223
+ const pendingKey = `bind:${groupId}`;
1068
1224
  const keystore = store._keystore; // AIDStore._keystore: LocalIdentityStore
1069
1225
  let keyPair = null;
1070
1226
  // 优先复用 pending 槽位中已暂存的待绑定密钥(崩溃/重试幂等)
1071
1227
  if (groupId && keystore && typeof keystore.loadPendingGroupBind === 'function') {
1072
- keyPair = keystore.loadPendingGroupBind(groupId);
1228
+ keyPair = keystore.loadPendingGroupBind(pendingKey);
1229
+ if (!keyPair || !keyPair.public_key_der_b64 || !keyPair.private_key_pem) {
1230
+ keyPair = keystore.loadPendingGroupBind(groupId);
1231
+ }
1073
1232
  }
1074
1233
  if (!keyPair || !keyPair.public_key_der_b64 || !keyPair.private_key_pem) {
1075
1234
  // 未命中 pending,生成新密钥
@@ -1081,7 +1240,7 @@ export class AUNClient {
1081
1240
  };
1082
1241
  // 发 RPC 前先落盘暂存,确保崩溃后重试能复用同一密钥
1083
1242
  if (groupId && keystore && typeof keystore.savePendingGroupBind === 'function') {
1084
- keystore.savePendingGroupBind(groupId, keyPair);
1243
+ keystore.savePendingGroupBind(pendingKey, keyPair);
1085
1244
  }
1086
1245
  }
1087
1246
  if (!keyPair) {
@@ -1113,8 +1272,10 @@ export class AUNClient {
1113
1272
  if (!imported.ok) {
1114
1273
  throw new ValidationError(imported.error.message);
1115
1274
  }
1275
+ await this._uploadGroupAgentMd(store, groupAid, payload, group);
1116
1276
  // 落盘成功,清除 pending 槽位
1117
1277
  if (groupId && keystore && typeof keystore.clearPendingGroupBind === 'function') {
1278
+ keystore.clearPendingGroupBind(pendingKey);
1118
1279
  keystore.clearPendingGroupBind(groupId);
1119
1280
  }
1120
1281
  return result;
@@ -1137,7 +1298,7 @@ export class AUNClient {
1137
1298
  const info = await this.call('group.get_info', { group_id: groupId, required: ['member'] });
1138
1299
  const infoMap = isJsonObject(info) ? info : {};
1139
1300
  const grp = isJsonObject(infoMap.group) ? infoMap.group : infoMap;
1140
- groupAid = String(info?.group_aid ?? '').trim();
1301
+ groupAid = String(info?.group_aid ?? grp?.group_aid ?? '').trim();
1141
1302
  }
1142
1303
  if (!groupAid) {
1143
1304
  throw new ValidationError('startGroupTransfer: unable to determine group_aid');
@@ -1185,7 +1346,7 @@ export class AUNClient {
1185
1346
  const info = await this.call('group.get_info', { group_id: groupId, required: ['member'] });
1186
1347
  const infoMap = isJsonObject(info) ? info : {};
1187
1348
  const grp = isJsonObject(infoMap.group) ? infoMap.group : infoMap;
1188
- groupAid = String(info?.group_aid ?? '').trim();
1349
+ groupAid = String(info?.group_aid ?? grp?.group_aid ?? '').trim();
1189
1350
  }
1190
1351
  if (!groupAid) {
1191
1352
  throw new ValidationError('renewGroupAid: unable to determine group_aid');
@@ -1204,7 +1365,21 @@ export class AUNClient {
1204
1365
  throw new ValidationError(`renewGroupAid: cannot determine old public key for ${groupAid}`);
1205
1366
  }
1206
1367
  // 生成新密钥
1207
- const newKeyPair = new CryptoProvider().generateIdentity();
1368
+ const keystore = store._keystore;
1369
+ const pendingKey = `renew:${groupId}`;
1370
+ let newKeyPair = null;
1371
+ if (keystore && typeof keystore.loadPendingGroupBind === 'function') {
1372
+ newKeyPair = keystore.loadPendingGroupBind(pendingKey);
1373
+ }
1374
+ if (!newKeyPair || !newKeyPair.public_key_der_b64 || !newKeyPair.private_key_pem) {
1375
+ newKeyPair = new CryptoProvider().generateIdentity();
1376
+ if (keystore && typeof keystore.savePendingGroupBind === 'function') {
1377
+ keystore.savePendingGroupBind(pendingKey, newKeyPair);
1378
+ }
1379
+ }
1380
+ if (!newKeyPair || !newKeyPair.public_key_der_b64 || !newKeyPair.private_key_pem) {
1381
+ throw new ValidationError('renewGroupAid: failed to generate or load key pair');
1382
+ }
1208
1383
  const newPublicKey = newKeyPair.public_key_der_b64;
1209
1384
  const newPrivateKey = newKeyPair.private_key_pem;
1210
1385
  const curve = newKeyPair.curve || 'P-256';
@@ -1259,6 +1434,10 @@ export class AUNClient {
1259
1434
  if (!imported.ok) {
1260
1435
  throw new ValidationError(`renewGroupAid failed to persist group identity: ${imported.error.message}`);
1261
1436
  }
1437
+ await this._uploadGroupAgentMd(store, returnedGroupAid, payload, group);
1438
+ if (keystore && typeof keystore.clearPendingGroupBind === 'function') {
1439
+ keystore.clearPendingGroupBind(pendingKey);
1440
+ }
1262
1441
  return result;
1263
1442
  }
1264
1443
  async completeGroupTransfer(params = {}, options = {}) {
@@ -1269,18 +1448,32 @@ export class AUNClient {
1269
1448
  if (!store) {
1270
1449
  throw new ValidationError('completeGroupTransfer requires aidStore');
1271
1450
  }
1272
- const keyPair = new CryptoProvider().generateIdentity();
1273
1451
  const payload = { ...params };
1274
1452
  const groupId = String(params.group_id ?? '').trim();
1275
1453
  if (!groupId) {
1276
1454
  throw new ValidationError('completeGroupTransfer requires group_id');
1277
1455
  }
1456
+ const pendingKey = `complete:${groupId}`;
1457
+ const keystore = store._keystore;
1458
+ let keyPair = null;
1459
+ if (keystore && typeof keystore.loadPendingGroupBind === 'function') {
1460
+ keyPair = keystore.loadPendingGroupBind(pendingKey);
1461
+ }
1462
+ if (!keyPair || !keyPair.public_key_der_b64 || !keyPair.private_key_pem) {
1463
+ keyPair = new CryptoProvider().generateIdentity();
1464
+ if (keystore && typeof keystore.savePendingGroupBind === 'function') {
1465
+ keystore.savePendingGroupBind(pendingKey, keyPair);
1466
+ }
1467
+ }
1468
+ if (!keyPair || !keyPair.public_key_der_b64 || !keyPair.private_key_pem) {
1469
+ throw new ValidationError('completeGroupTransfer: failed to generate or load key pair');
1470
+ }
1278
1471
  let groupAid = String(params.group_aid ?? '').trim();
1279
1472
  if (!groupAid) {
1280
1473
  const info = await this.call('group.get_info', { group_id: groupId, required: ['member'] });
1281
1474
  const infoMap = isJsonObject(info) ? info : {};
1282
1475
  const grp = isJsonObject(infoMap.group) ? infoMap.group : infoMap;
1283
- groupAid = String(info?.group_aid ?? '').trim();
1476
+ groupAid = String(info?.group_aid ?? grp?.group_aid ?? '').trim();
1284
1477
  }
1285
1478
  if (!groupAid) {
1286
1479
  throw new ValidationError('completeGroupTransfer: unable to determine group_aid');
@@ -1330,31 +1523,25 @@ export class AUNClient {
1330
1523
  if (!imported.ok) {
1331
1524
  throw new ValidationError(imported.error.message);
1332
1525
  }
1526
+ await this._uploadGroupAgentMd(store, returnedGroupAid, payload, group);
1527
+ if (keystore && typeof keystore.clearPendingGroupBind === 'function') {
1528
+ keystore.clearPendingGroupBind(pendingKey);
1529
+ }
1333
1530
  return result;
1334
1531
  }
1335
1532
  /** 证书公钥校验:确保服务端返回的证书公钥 == 本地公钥(DER base64) */
1336
1533
  async verifyCertPublicKeyMatch(certPem, localPublicKeyDerB64, operation) {
1337
- // 简化实现:使用 Node.js crypto 或浏览器 Web Crypto API
1338
- // 完整实现需要解析 X.509 证书提取 subjectPublicKeyInfo
1339
- // 这里先做基本校验,后续可增强
1340
- const crypto = globalThis.crypto;
1341
- if (!crypto || !crypto.subtle) {
1342
- // 浏览器环境或 Node < 15,跳过校验(非致命)
1343
- console.warn(`${operation}: crypto.subtle not available, skipping cert public key verification`);
1344
- return;
1345
- }
1346
1534
  try {
1347
- // TODO: 完整实现需要 ASN.1 解析库提取证书公钥
1348
- // 当前仅做导入测试,确保证书格式有效
1349
- const certLines = certPem.split('\n').filter(l => l && !l.startsWith('-----'));
1350
- const certDer = Buffer.from(certLines.join(''), 'base64');
1351
- // 验证本地公钥可导入
1535
+ const cert = new crypto.X509Certificate(certPem);
1536
+ const certPubDer = cert.publicKey.export({ type: 'spki', format: 'der' });
1352
1537
  const localPubDer = Buffer.from(localPublicKeyDerB64, 'base64');
1353
- await crypto.subtle.importKey('spki', localPubDer, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']);
1354
- // 证书公钥提取需要完整 ASN.1 解析,暂时依赖服务端正确性
1355
- // 后续迭代可引入 @peculiar/x509 或类似库
1538
+ if (!localPubDer.length || !certPubDer.equals(localPubDer)) {
1539
+ throw new AuthError(`${operation}: certificate public key mismatch`);
1540
+ }
1356
1541
  }
1357
1542
  catch (err) {
1543
+ if (err instanceof AuthError)
1544
+ throw err;
1358
1545
  throw new AuthError(`${operation}: certificate or public key validation failed: ${err.message}`);
1359
1546
  }
1360
1547
  }
@@ -1566,9 +1753,11 @@ export class AUNClient {
1566
1753
  this._clientLog.debug(`P2P pending pull postponed: ns=${ns}, upper_seq=${upperSeq}, contiguous=${contig}, state=${this._state}, closing=${this._closing}, reason=${reason}`);
1567
1754
  return false;
1568
1755
  }
1756
+ if (this._rpcPipeline?.hasPullActivity?.(ns, false))
1757
+ return false;
1569
1758
  this._pendingP2pPullUpper.delete(ns);
1570
1759
  this._clientLog.info(`P2P pending push follow-up pull scheduled: ns=${ns}, upper_seq=${upperSeq}, contiguous=${contig}, reason=${reason}`);
1571
- void this._fillP2pGap();
1760
+ this._safeAsync(this._onV2PushNotification({ seq: upperSeq }, 'v2'));
1572
1761
  return true;
1573
1762
  }
1574
1763
  _markPublishedSeq(ns, seq) {
@@ -1707,18 +1896,6 @@ export class AUNClient {
1707
1896
  }
1708
1897
  return Math.max(0, ...values.filter((value) => Number.isFinite(value)));
1709
1898
  }
1710
- _explicitGroupCursorParams(params) {
1711
- const value = params._group_cursor_params;
1712
- if (!isJsonObject(value))
1713
- return {};
1714
- return { ...value };
1715
- }
1716
- _groupCursorTargetsCurrentInstance(params) {
1717
- const deviceId = String(params.device_id ?? '').trim();
1718
- const slotId = String(params.slot_id ?? '').trim();
1719
- return (!deviceId || deviceId === (this._deviceId ?? ''))
1720
- && (!slotId || slotId === (this._slotId ?? ''));
1721
- }
1722
1899
  async _withBackgroundRpc(operation) {
1723
1900
  this._backgroundRpcDepth += 1;
1724
1901
  try {
@@ -1731,6 +1908,12 @@ export class AUNClient {
1731
1908
  async _runPullSerialized(key, operation, background = false) {
1732
1909
  return await this._rpcPipeline.runPullSerialized(key, operation, background);
1733
1910
  }
1911
+ /** Mail bridge facade 入口 */
1912
+ get mail() {
1913
+ if (!this._mail)
1914
+ this._mail = new MailFacade(this);
1915
+ return this._mail;
1916
+ }
1734
1917
  async _tryRunInlineRealtime(namespace, seq, operation) {
1735
1918
  return await this._rpcPipeline.tryRunInlineRealtime(namespace, seq, operation);
1736
1919
  }
@@ -1760,14 +1943,14 @@ export class AUNClient {
1760
1943
  }
1761
1944
  return true;
1762
1945
  }
1763
- async _drainOrderedMessages(ns, beforeSeq, pullResponse = false) {
1764
- return this._delivery.drainOrderedMessages(ns, beforeSeq, pullResponse);
1946
+ async _drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true) {
1947
+ return this._delivery.drainOrderedMessages(ns, beforeSeq, pullResponse, persist);
1765
1948
  }
1766
1949
  async _publishOrderedMessage(event, ns, seq, payload) {
1767
1950
  return this._delivery.publishOrderedMessage(event, ns, seq, payload);
1768
1951
  }
1769
- async _publishPulledMessage(event, ns, seq, payload) {
1770
- return this._delivery.publishPulledMessage(event, ns, seq, payload);
1952
+ async _publishPulledMessage(event, ns, seq, payload, persist = true) {
1953
+ return this._delivery.publishPulledMessage(event, ns, seq, payload, persist);
1771
1954
  }
1772
1955
  _markOrderedSeqDelivered(ns, seq) {
1773
1956
  if (!ns || !Number.isFinite(seq) || !Number.isInteger(seq) || seq <= 0)
@@ -1836,7 +2019,7 @@ export class AUNClient {
1836
2019
  this._seqTracker.removeNamespace(groupEventNs);
1837
2020
  this._persistRepairedSeq(groupNs);
1838
2021
  this._persistRepairedSeq(groupEventNs);
1839
- this._saveSeqTrackerState();
2022
+ void this._saveSeqTrackerState();
1840
2023
  for (const key of this._gapFillDone.keys()) {
1841
2024
  if (key.includes(groupId)) {
1842
2025
  this._gapFillDone.delete(key);
@@ -1929,7 +2112,8 @@ export class AUNClient {
1929
2112
  if (!normalizeFingerprintHex(expectedFP)) {
1930
2113
  throw new ValidationError(`unsupported cert_fingerprint format for ${aid}: ${expectedFP.slice(0, 24)}`);
1931
2114
  }
1932
- if (!certMatchesFingerprint(certPem, expectedFP)) {
2115
+ if (!certMatchesFingerprint(certPem, expectedFP)
2116
+ && !publicKeyMatchesFingerprint(certPem, expectedFP)) {
1933
2117
  throw new ValidationError(`peer cert fingerprint mismatch for ${aid}: expected=${expectedFP.slice(0, 24)}...`);
1934
2118
  }
1935
2119
  }
@@ -2103,8 +2287,9 @@ export class AUNClient {
2103
2287
  return JSON.stringify([this._aid, this._deviceId, this._slotId]);
2104
2288
  }
2105
2289
  _resetSeqTrackingState() {
2106
- this._saveSeqTrackerState();
2290
+ void this._saveSeqTrackerState();
2107
2291
  this._resetV2IdentityRuntime();
2292
+ this._v2E2EE.resetForwardCursorState();
2108
2293
  this._delivery.resetInlineAckState();
2109
2294
  this._seqTracker = new SeqTracker();
2110
2295
  this._seqTrackerContext = null;
@@ -2115,18 +2300,13 @@ export class AUNClient {
2115
2300
  this._v2SenderIKPending.clear();
2116
2301
  this._v2SenderIKFetching.clear();
2117
2302
  this._groupSynced.clear();
2118
- this._onlineUnreadHintQueue.clear();
2119
- if (this._onlineUnreadHintTimer) {
2120
- clearTimeout(this._onlineUnreadHintTimer);
2121
- this._onlineUnreadHintTimer = null;
2122
- }
2123
- this._onlineUnreadHintDrainActive = false;
2124
2303
  }
2125
2304
  _refreshSeqTrackerContext() {
2126
2305
  const nextContext = this._currentSeqTrackerContext();
2127
2306
  if (nextContext === this._seqTrackerContext)
2128
2307
  return;
2129
- this._saveSeqTrackerState();
2308
+ void this._saveSeqTrackerState();
2309
+ this._v2E2EE.resetForwardCursorState();
2130
2310
  this._delivery.resetInlineAckState();
2131
2311
  this._seqTracker = new SeqTracker();
2132
2312
  this._gapFillDone.clear();
@@ -2136,12 +2316,6 @@ export class AUNClient {
2136
2316
  this._v2SenderIKPending.clear();
2137
2317
  this._v2SenderIKFetching.clear();
2138
2318
  this._groupSynced.clear();
2139
- this._onlineUnreadHintQueue.clear();
2140
- if (this._onlineUnreadHintTimer) {
2141
- clearTimeout(this._onlineUnreadHintTimer);
2142
- this._onlineUnreadHintTimer = null;
2143
- }
2144
- this._onlineUnreadHintDrainActive = false;
2145
2319
  this._seqTrackerContext = nextContext;
2146
2320
  }
2147
2321
  /** 将 SeqTracker 状态保存到 keystore */
@@ -2210,8 +2384,10 @@ export class AUNClient {
2210
2384
  }
2211
2385
  // ── 内部:连接 ────────────────────────────────────────────
2212
2386
  /** 执行一次连接流程 */
2213
- async _connectOnce(params, allowReauth) {
2387
+ async _connectOnce(params, allowReauth, reconnectOwner, connectionOwner) {
2214
2388
  const tStart = Date.now();
2389
+ connectionOwner ??= this._connectAbort ?? undefined;
2390
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
2215
2391
  const gatewayUrl = this._resolveGateway(params);
2216
2392
  this._gatewayUrl = gatewayUrl;
2217
2393
  this._slotId = String(params.slot_id ?? '');
@@ -2228,8 +2404,11 @@ export class AUNClient {
2228
2404
  // 避免 reader 把积压 push 交给空 tracker 的 handler,触发 S2 历史 gap 误补拉。
2229
2405
  this._refreshSeqTrackerContext();
2230
2406
  this._restoreSeqTrackerState();
2407
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
2231
2408
  try {
2232
2409
  const challenge = await this._transport.connect(gatewayUrl);
2410
+ this._assertReconnectOwner(reconnectOwner);
2411
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
2233
2412
  this._clientLog.debug(`WebSocket connection established: gateway=${gatewayUrl}`);
2234
2413
  this._state = 'authenticating';
2235
2414
  if (allowReauth) {
@@ -2242,6 +2421,8 @@ export class AUNClient {
2242
2421
  shortTtlMs: Number(params.short_ttl_ms ?? 0),
2243
2422
  extraInfo,
2244
2423
  });
2424
+ this._assertReconnectOwner(reconnectOwner);
2425
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
2245
2426
  if (isJsonObject(authContext)) {
2246
2427
  const auth = authContext;
2247
2428
  const identity = auth.identity;
@@ -2268,11 +2449,15 @@ export class AUNClient {
2268
2449
  shortTtlMs: Number(params.short_ttl_ms ?? 0),
2269
2450
  extraInfo,
2270
2451
  });
2452
+ this._assertReconnectOwner(reconnectOwner);
2453
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
2271
2454
  this._syncIdentityAfterConnect(String(params.access_token));
2272
2455
  if (isJsonObject(hello) && 'heartbeat_interval' in hello) {
2273
2456
  this._applyServerHeartbeatInterval(hello.heartbeat_interval, 'auth');
2274
2457
  }
2275
2458
  }
2459
+ this._assertReconnectOwner(reconnectOwner);
2460
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
2276
2461
  if (!this._transport.isConnected()) {
2277
2462
  throw new ConnectionError('websocket disconnected during authentication');
2278
2463
  }
@@ -2288,13 +2473,29 @@ export class AUNClient {
2288
2473
  this._gatewayCandidates = [gatewayUrl];
2289
2474
  }
2290
2475
  this._clientLog.debug(`auth complete, connection ready: aid=${this._aid ?? ''}, gateway=${gatewayUrl}`);
2291
- await this._dispatcher.publish('state_change', { state: this._publicState(this._state), gateway: gatewayUrl });
2476
+ const statePayload = { state: this._publicState(this._state), gateway: gatewayUrl };
2477
+ if (reconnectOwner) {
2478
+ if (!await this._publishReconnectEvent(reconnectOwner, 'state_change', statePayload)) {
2479
+ throw new ConnectionError('reconnect attempt superseded');
2480
+ }
2481
+ }
2482
+ else if (connectionOwner) {
2483
+ if (!await this._lifecycle.publishConnectionEvent(connectionOwner, 'state_change', statePayload)) {
2484
+ throw new StateError('connection attempt superseded');
2485
+ }
2486
+ }
2487
+ else {
2488
+ await this._dispatcher.publish('state_change', statePayload);
2489
+ }
2490
+ this._assertReconnectOwner(reconnectOwner);
2491
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
2292
2492
  // auth 阶段 aid 可能被 identity 覆盖(上方 this._aid = identity.aid);
2293
2493
  // 若 context 发生变化,重新 refresh + restore,保持 tracker 与真实身份一致。
2294
2494
  if (this._seqTrackerContext !== this._currentSeqTrackerContext()) {
2295
2495
  this._refreshSeqTrackerContext();
2296
2496
  this._restoreSeqTrackerState();
2297
2497
  }
2498
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
2298
2499
  this._startBackgroundTasks();
2299
2500
  const connectionKind = String(params.connection_kind ?? 'long');
2300
2501
  const isShortConnection = connectionKind === 'short';
@@ -2303,6 +2504,8 @@ export class AUNClient {
2303
2504
  && (!isShortConnection || hasExplicitBackgroundSync);
2304
2505
  if (!isShortConnection) {
2305
2506
  await this._v2E2EE.onConnected({ backgroundSync: backgroundSyncEnabled });
2507
+ this._assertReconnectOwner(reconnectOwner);
2508
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
2306
2509
  }
2307
2510
  else {
2308
2511
  this._clientLog.debug('V2 session init deferred for short connection');
@@ -2317,7 +2520,11 @@ export class AUNClient {
2317
2520
  this._clientLog.debug(`_connectOnce exit: elapsed=${Date.now() - tStart}ms gateway=${gatewayUrl}, aid=${this._aid ?? ''}`);
2318
2521
  }
2319
2522
  catch (err) {
2320
- this._state = (prevState === 'connected' || prevState === 'ready') ? 'standby' : (this._currentAid ? 'standby' : 'no_identity');
2523
+ if ((!reconnectOwner || this._ownsReconnect(reconnectOwner))
2524
+ && (!connectionOwner || this._lifecycle.ownsConnectionAttempt(connectionOwner))
2525
+ && !this._closing) {
2526
+ this._state = (prevState === 'connected' || prevState === 'ready') ? 'standby' : (this._currentAid ? 'standby' : 'no_identity');
2527
+ }
2321
2528
  this._clientLog.debug(`_connectOnce exit (error): elapsed=${Date.now() - tStart}ms gateway=${gatewayUrl} err=${err instanceof Error ? err.message : String(err)}`);
2322
2529
  throw err;
2323
2530
  }
@@ -2523,7 +2730,7 @@ export class AUNClient {
2523
2730
  return await this._v2E2EE.ackGroupV2(groupId, upToSeq, groupAid);
2524
2731
  }
2525
2732
  /** 解密单条 V2 pull 消息。缺 sender IK 时先入 pending,后台补齐后重试。 */
2526
- async _decryptV2Message(msg, allowPending = true, emitUndecryptable = true, rotateKeys = true, observeAgentMd = true) {
2733
+ async _decryptV2Message(msg, allowPending = true, emitUndecryptable = true, rotateKeys = true, observeAgentMd = true, deferStatus, deferKeyFetch = false) {
2527
2734
  const session = this._v2Session;
2528
2735
  if (!session)
2529
2736
  return null;
@@ -2585,6 +2792,24 @@ export class AUNClient {
2585
2792
  }
2586
2793
  catch (exc) {
2587
2794
  this._clientLog.warn(`V2 decrypt: SPK lookup failed seq=${String(msg.seq)} spk_id=${spkId}: ${formatCaughtError(exc)}`);
2795
+ if (allowPending || deferStatus) {
2796
+ const fromAid = String(msg.from_aid ?? '');
2797
+ const senderDeviceId = String(aad.from_device ?? '');
2798
+ if (deferStatus) {
2799
+ deferStatus.deferred = true;
2800
+ deferStatus.fromAid = fromAid;
2801
+ deferStatus.senderDeviceId = senderDeviceId;
2802
+ deferStatus.groupId = groupIdForKeys;
2803
+ }
2804
+ this._scheduleV2SenderIKPending({
2805
+ msg,
2806
+ fromAid,
2807
+ senderDeviceId,
2808
+ groupId: groupIdForKeys,
2809
+ startFetch: !deferKeyFetch,
2810
+ });
2811
+ return null;
2812
+ }
2588
2813
  const event = {
2589
2814
  message_id: String(msg.message_id ?? ''),
2590
2815
  from: String(msg.from_aid ?? ''),
@@ -2610,13 +2835,19 @@ export class AUNClient {
2610
2835
  const senderDeviceId = String(aad.from_device ?? '');
2611
2836
  const senderCertFingerprint = String(envelope.sender_cert_fingerprint ?? '').trim().toLowerCase();
2612
2837
  let senderPubDer = await this._getV2SenderPubDer(fromAid, senderDeviceId, senderCertFingerprint);
2613
- if (!senderPubDer) {
2838
+ if (!senderPubDer && !deferKeyFetch) {
2614
2839
  this._clientLog.warn(`V2 decrypt: no sender IK for ${fromAid} device=${senderDeviceId}`);
2615
2840
  senderPubDer = await this._fetchV2SenderIKOnce(fromAid, senderDeviceId, groupIdForKeys, senderCertFingerprint);
2616
2841
  }
2617
2842
  if (!senderPubDer) {
2618
- if (allowPending) {
2619
- this._scheduleV2SenderIKPending({ msg, fromAid, senderDeviceId, groupId: groupIdForKeys });
2843
+ if (allowPending || deferStatus) {
2844
+ if (deferStatus) {
2845
+ deferStatus.deferred = true;
2846
+ deferStatus.fromAid = fromAid;
2847
+ deferStatus.senderDeviceId = senderDeviceId;
2848
+ deferStatus.groupId = groupIdForKeys;
2849
+ }
2850
+ this._scheduleV2SenderIKPending({ msg, fromAid, senderDeviceId, groupId: groupIdForKeys, startFetch: !deferKeyFetch });
2620
2851
  return null;
2621
2852
  }
2622
2853
  const event = {
@@ -2641,9 +2872,13 @@ export class AUNClient {
2641
2872
  let plaintext;
2642
2873
  try {
2643
2874
  plaintext = decryptMessage(envelope, this._aid ?? '', this._deviceId, ikPriv, spkPriv, senderPubDer);
2875
+ if (plaintext === null)
2876
+ throw new Error('recipient_not_found_or_envelope_verification_failed');
2644
2877
  }
2645
2878
  catch (exc) {
2646
2879
  this._clientLog.warn(`V2 decrypt failed for msg seq=${String(msg.seq)}: ${formatCaughtError(exc)}`);
2880
+ if (deferStatus)
2881
+ deferStatus.terminal = true;
2647
2882
  const event = {
2648
2883
  message_id: String(msg.message_id ?? ''),
2649
2884
  from: fromAid,
@@ -2896,6 +3131,11 @@ export class AUNClient {
2896
3131
  if (!resolvedAid) {
2897
3132
  throw new StateError('gateway discovery requires a loaded AID');
2898
3133
  }
3134
+ const discovery = this._discovery;
3135
+ const tokenStore = this._tokenStore;
3136
+ const discoveryIsCurrent = () => (this._discovery === discovery
3137
+ && this._tokenStore === tokenStore
3138
+ && !this._closing);
2899
3139
  if (this._gatewayCandidates.length > 0)
2900
3140
  return [...this._gatewayCandidates];
2901
3141
  if (this._gatewayUrl) {
@@ -2903,9 +3143,9 @@ export class AUNClient {
2903
3143
  return [...this._gatewayCandidates];
2904
3144
  }
2905
3145
  try {
2906
- const loadMetadata = this._tokenStore.loadMetadata;
3146
+ const loadMetadata = tokenStore.loadMetadata;
2907
3147
  const metadata = typeof loadMetadata === 'function'
2908
- ? loadMetadata.call(this._tokenStore, resolvedAid)
3148
+ ? loadMetadata.call(tokenStore, resolvedAid)
2909
3149
  : null;
2910
3150
  const cachedList = isJsonObject(metadata) && Array.isArray(metadata.gateway_urls)
2911
3151
  ? metadata.gateway_urls.map((item) => String(item ?? '').trim()).filter(Boolean)
@@ -2932,12 +3172,13 @@ export class AUNClient {
2932
3172
  try {
2933
3173
  // discoverAll 是生产路径;若调用方注入了旧版/测试 discovery
2934
3174
  // 替身而只提供 discover,则保留单候选兼容,不影响多候选发现。
2935
- const discovery = this._discovery;
2936
3175
  const hasInjectedDiscover = Object.prototype.hasOwnProperty.call(discovery, 'discover')
2937
3176
  && typeof discovery.discover === 'function';
2938
3177
  const discovered = hasInjectedDiscover
2939
3178
  ? [await discovery.discover(url)]
2940
3179
  : await discovery.discoverAll(url);
3180
+ if (!discoveryIsCurrent())
3181
+ throw new StateError('gateway discovery superseded by identity change');
2941
3182
  const gatewayUrls = [...new Set(discovered.map((item) => String(item ?? '').trim()).filter(Boolean))];
2942
3183
  if (gatewayUrls.length === 0)
2943
3184
  throw new ValidationError('gateway discovery returned no candidates');
@@ -2945,9 +3186,9 @@ export class AUNClient {
2945
3186
  const gateway = gatewayUrls[0];
2946
3187
  this._gatewayUrl = gateway;
2947
3188
  try {
2948
- const saveMetadata = this._tokenStore.saveMetadata;
3189
+ const saveMetadata = tokenStore.saveMetadata;
2949
3190
  if (typeof saveMetadata === 'function') {
2950
- saveMetadata.call(this._tokenStore, resolvedAid, {
3191
+ saveMetadata.call(tokenStore, resolvedAid, {
2951
3192
  gateway_url: gateway,
2952
3193
  gateway_urls: gatewayUrls,
2953
3194
  gateway_cached_at: Date.now(),
@@ -2960,6 +3201,8 @@ export class AUNClient {
2960
3201
  return [...gatewayUrls];
2961
3202
  }
2962
3203
  catch (err) {
3204
+ if (!discoveryIsCurrent())
3205
+ throw new StateError('gateway discovery superseded by identity change');
2963
3206
  lastErr = err;
2964
3207
  this._clientLog.warn(`gateway discovery failed: aid=${resolvedAid} url=${url} err=${formatCaughtError(err)}`);
2965
3208
  }
@@ -3311,8 +3554,8 @@ export class AUNClient {
3311
3554
  }
3312
3555
  }
3313
3556
  /** 传输层断线回调 */
3314
- async _handleTransportDisconnect(error, closeCode) {
3315
- if (this._closing || this.state === ConnectionState.CLOSED)
3557
+ async _handleTransportDisconnect(error, closeCode, initialOptions) {
3558
+ if (this._closing || this._disconnecting || this.state === ConnectionState.CLOSED)
3316
3559
  return;
3317
3560
  // 已在重连中则跳过,避免心跳超时和 transport 断线回调重复触发
3318
3561
  if (this._reconnectActive)
@@ -3321,6 +3564,9 @@ export class AUNClient {
3321
3564
  this._delivery.resetInlineAckState();
3322
3565
  // 原子化标志,防止 await 期间并发调用再次进入
3323
3566
  this._reconnectActive = true;
3567
+ // 在首个 await 前创建取消控制器,覆盖 disconnect 与清理动作的竞态窗口。
3568
+ const reconnectAbort = new AbortController();
3569
+ this._reconnectAbort = reconnectAbort;
3324
3570
  this._clientLog.warn(`transport disconnected: closeCode=${closeCode ?? 'none'}, error=${error ? formatCaughtError(error) : 'none'}`);
3325
3571
  this._state = 'standby';
3326
3572
  this._stopBackgroundTasks();
@@ -3330,9 +3576,28 @@ export class AUNClient {
3330
3576
  catch (exc) {
3331
3577
  this._clientLog.debug(`transport cleanup skipped: ${formatCaughtError(exc)}`);
3332
3578
  }
3333
- await this._dispatcher.publish('state_change', { state: this._publicState(this._state), error });
3579
+ if (!await this._publishReconnectEvent(reconnectAbort, 'state_change', {
3580
+ state: this._publicState(this._state),
3581
+ error,
3582
+ })) {
3583
+ if (this._reconnectAbort === reconnectAbort) {
3584
+ this._reconnectAbort = null;
3585
+ this._reconnectActive = false;
3586
+ }
3587
+ return;
3588
+ }
3589
+ if (reconnectAbort.signal.aborted || this._reconnectAbort !== reconnectAbort || this._closing) {
3590
+ if (this._reconnectAbort === reconnectAbort) {
3591
+ this._reconnectAbort = null;
3592
+ this._reconnectActive = false;
3593
+ }
3594
+ return;
3595
+ }
3334
3596
  if (!this._sessionOptions.auto_reconnect) {
3335
- this._reconnectActive = false;
3597
+ if (this._reconnectAbort === reconnectAbort) {
3598
+ this._reconnectAbort = null;
3599
+ this._reconnectActive = false;
3600
+ }
3336
3601
  return;
3337
3602
  }
3338
3603
  // 不重连 close code(认证失败/权限错误/被踢等)或服务端通知断开:抑制重连
@@ -3352,38 +3617,111 @@ export class AUNClient {
3352
3617
  if (disconnectInfo.code !== undefined && disconnectInfo.code !== null) {
3353
3618
  eventPayload.code = disconnectInfo.code;
3354
3619
  }
3355
- await this._dispatcher.publish('state_change', eventPayload);
3356
- this._reconnectActive = false;
3620
+ await this._publishReconnectEvent(reconnectAbort, 'state_change', eventPayload);
3621
+ if (this._reconnectAbort === reconnectAbort) {
3622
+ this._reconnectAbort = null;
3623
+ this._reconnectActive = false;
3624
+ }
3357
3625
  return;
3358
3626
  }
3359
3627
  // 1000 = 正常关闭, 1006 = 网络异常断开(无 close frame),其他 code = 服务端主动关闭
3360
3628
  const serverInitiated = closeCode !== undefined && closeCode !== 1000 && closeCode !== 1006;
3361
- this._reconnectActive = false;
3362
- this._startReconnect(serverInitiated);
3629
+ if (reconnectAbort.signal.aborted || this._reconnectAbort !== reconnectAbort || this._closing) {
3630
+ if (this._reconnectAbort === reconnectAbort) {
3631
+ this._reconnectAbort = null;
3632
+ this._reconnectActive = false;
3633
+ }
3634
+ return;
3635
+ }
3636
+ this._startReconnect(serverInitiated, initialOptions, reconnectAbort);
3363
3637
  }
3364
3638
  /** 启动重连循环(默认无限重试 + 当前基数抖动,仅在不可重试错误、close() 或 max_attempts 耗尽时终止) */
3365
- _startReconnect(serverInitiated = false) {
3366
- if (this._reconnectActive)
3367
- return;
3368
- this._reconnectActive = true;
3369
- const reconnectAbort = new AbortController();
3370
- this._reconnectAbort = reconnectAbort;
3639
+ _startReconnect(serverInitiated = false, initialOptions, existingAbort) {
3640
+ if (existingAbort) {
3641
+ if (existingAbort.signal.aborted || this._reconnectAbort !== existingAbort)
3642
+ return;
3643
+ }
3644
+ else {
3645
+ if (this._reconnectActive)
3646
+ return;
3647
+ this._reconnectActive = true;
3648
+ this._reconnectAbort = new AbortController();
3649
+ }
3650
+ const reconnectAbort = existingAbort ?? this._reconnectAbort;
3371
3651
  this._clientLog.debug(`reconnect loop started: serverInitiated=${String(serverInitiated)}, aid=${this._aid ?? ''}`);
3372
- this._reconnectLoop(serverInitiated, reconnectAbort).catch((exc) => {
3373
- this._clientLog.warn(`reconnect loop error: ${formatCaughtError(exc)}`);
3374
- });
3652
+ if (!reconnectAbort)
3653
+ return;
3654
+ const reconnectTask = Promise.resolve().then(() => this._reconnectLoop(serverInitiated, reconnectAbort, initialOptions));
3655
+ this._reconnectTask = reconnectTask;
3656
+ this._safeAsync(reconnectTask);
3657
+ }
3658
+ _ownsReconnect(owner) {
3659
+ return Boolean(owner
3660
+ && !owner.signal.aborted
3661
+ && !this._closing
3662
+ && this._reconnectAbort === owner);
3663
+ }
3664
+ _assertReconnectOwner(owner) {
3665
+ if (owner && !this._ownsReconnect(owner)) {
3666
+ throw new ConnectionError('reconnect attempt superseded');
3667
+ }
3668
+ }
3669
+ async _publishReconnectEvent(owner, event, payload) {
3670
+ if (!this._ownsReconnect(owner))
3671
+ return false;
3672
+ this._reconnectEventDispatchDepth += 1;
3673
+ try {
3674
+ await this._dispatcher.publish(event, payload);
3675
+ }
3676
+ finally {
3677
+ this._reconnectEventDispatchDepth -= 1;
3678
+ }
3679
+ return this._ownsReconnect(owner);
3680
+ }
3681
+ async _cancelReconnectAndWait() {
3682
+ const owner = this._reconnectAbort;
3683
+ const task = this._reconnectTask;
3684
+ if (!owner && !task && !this._reconnectActive)
3685
+ return;
3686
+ owner?.abort();
3687
+ this._discovery?._cancelPending?.();
3688
+ this._auth?._cancelPending?.();
3689
+ this._stopBackgroundTasks();
3690
+ try {
3691
+ await this._transport.close();
3692
+ }
3693
+ catch (exc) {
3694
+ this._clientLog.debug(`reconnect cancellation transport cleanup skipped: ${formatCaughtError(exc)}`);
3695
+ }
3696
+ // 重连事件 handler 内等待所属任务会自锁;事件返回后的 owner 复核负责收尾。
3697
+ if (task && this._reconnectEventDispatchDepth === 0) {
3698
+ try {
3699
+ await task;
3700
+ }
3701
+ catch (exc) {
3702
+ this._clientLog.debug(`cancelled reconnect task exited with error: ${formatCaughtError(exc)}`);
3703
+ }
3704
+ }
3705
+ if (this._reconnectAbort === owner) {
3706
+ this._reconnectAbort = null;
3707
+ this._reconnectActive = false;
3708
+ this._nextRetryAt = null;
3709
+ }
3710
+ if (this._reconnectTask === task)
3711
+ this._reconnectTask = null;
3375
3712
  }
3376
3713
  /** 重连循环入口;无论正常退出还是意外异常,都只清理自己这一代控制器。 */
3377
- async _reconnectLoop(serverInitiated, reconnectAbort = this._reconnectAbort) {
3714
+ async _reconnectLoop(serverInitiated, reconnectAbort = this._reconnectAbort, initialOptions) {
3378
3715
  let releasedOwnGeneration = false;
3379
3716
  try {
3380
- await this._runReconnectLoop(serverInitiated, reconnectAbort);
3717
+ await this._runReconnectLoop(serverInitiated, reconnectAbort, initialOptions);
3381
3718
  }
3382
3719
  finally {
3383
3720
  if (this._reconnectAbort === reconnectAbort) {
3384
3721
  this._nextRetryAt = null;
3385
3722
  this._reconnectActive = false;
3386
3723
  this._reconnectAbort = null;
3724
+ this._reconnectTask = null;
3387
3725
  releasedOwnGeneration = true;
3388
3726
  }
3389
3727
  }
@@ -3394,44 +3732,47 @@ export class AUNClient {
3394
3732
  }
3395
3733
  }
3396
3734
  /** 重连循环主体(for 循环 + AbortController,与 JS/Python 对齐) */
3397
- async _runReconnectLoop(serverInitiated, reconnectAbort) {
3735
+ async _runReconnectLoop(serverInitiated, reconnectAbort, initialOptions) {
3398
3736
  const retry = this._sessionOptions.retry;
3399
3737
  const maxBaseDelay = clampReconnectDelayMs(Number(retry.max_delay ?? 64.0) * 1000, RECONNECT_MAX_BASE_DELAY_MS);
3400
3738
  const maxAttemptsRaw = Number(retry.max_attempts ?? 0);
3401
3739
  const maxAttempts = Number.isFinite(maxAttemptsRaw) && maxAttemptsRaw > 0 ? Math.floor(maxAttemptsRaw) : 0;
3402
3740
  this._retryMaxAttempts = maxAttempts;
3403
3741
  let delay = clampReconnectDelayMs(serverInitiated ? 16_000 : Number(retry.initial_delay ?? 1.0) * 1000, serverInitiated ? 16_000 : RECONNECT_MIN_BASE_DELAY_MS, maxBaseDelay);
3404
- for (let attempt = 1; !reconnectAbort?.signal.aborted; attempt++) {
3405
- if (this._closing)
3406
- break;
3742
+ if (!this._ownsReconnect(reconnectAbort))
3743
+ return;
3744
+ for (let attempt = 1; this._ownsReconnect(reconnectAbort); attempt++) {
3407
3745
  // max_attempts 检查在循环顶部,覆盖所有路径(含 health-fail)
3408
3746
  if (maxAttempts > 0 && attempt > maxAttempts) {
3409
3747
  this._state = 'connection_failed';
3410
- await this._dispatcher.publish('state_change', {
3411
- state: this._publicState(this._state),
3412
- attempt: attempt - 1,
3413
- reason: 'max_attempts_exhausted',
3414
- });
3748
+ if (reconnectAbort)
3749
+ await this._publishReconnectEvent(reconnectAbort, 'state_change', {
3750
+ state: this._publicState(this._state),
3751
+ attempt: attempt - 1,
3752
+ reason: 'max_attempts_exhausted',
3753
+ });
3415
3754
  break;
3416
3755
  }
3417
3756
  this._retryAttempt = attempt;
3418
3757
  this._nextRetryAt = Date.now() + reconnectSleepDelayMs(delay, maxBaseDelay);
3419
3758
  this._state = 'retry_backoff';
3420
- await this._dispatcher.publish('state_change', {
3759
+ if (!reconnectAbort || !await this._publishReconnectEvent(reconnectAbort, 'state_change', {
3421
3760
  state: this._publicState(this._state),
3422
3761
  attempt,
3423
3762
  next_retry_at: this._nextRetryAt,
3424
- });
3763
+ }))
3764
+ return;
3425
3765
  try {
3426
3766
  // 抖动随当前 base 增长且不超过 max_base,避免首次重连被放大到几十秒。
3427
3767
  await this._sleep(Math.max(0, this._nextRetryAt - Date.now()), reconnectAbort?.signal);
3428
- if (reconnectAbort?.signal.aborted || this._closing)
3429
- break;
3768
+ if (!this._ownsReconnect(reconnectAbort))
3769
+ return;
3430
3770
  this._state = 'reconnecting';
3431
- await this._dispatcher.publish('state_change', {
3771
+ if (!reconnectAbort || !await this._publishReconnectEvent(reconnectAbort, 'state_change', {
3432
3772
  state: this._publicState(this._state),
3433
3773
  attempt,
3434
- });
3774
+ }))
3775
+ return;
3435
3776
  // /health 仅作旁路诊断,不能阻止实际 WebSocket 重连。
3436
3777
  if (this._gatewayUrl) {
3437
3778
  void this._discovery.checkHealth(this._gatewayUrl, 5_000)
@@ -3444,6 +3785,20 @@ export class AUNClient {
3444
3785
  });
3445
3786
  }
3446
3787
  await this._transport.close();
3788
+ if (!this._ownsReconnect(reconnectAbort))
3789
+ return;
3790
+ if (initialOptions) {
3791
+ // 首连失败时没有可复用的 session/gateway,必须重新 discovery + authenticate。
3792
+ await this._lifecycle.retryInitialConnect(initialOptions, reconnectAbort ?? undefined);
3793
+ if (!this._ownsReconnect(reconnectAbort))
3794
+ return;
3795
+ if (!this._transport.isConnected()) {
3796
+ throw new ConnectionError('websocket disconnected while initial reconnect completed');
3797
+ }
3798
+ this._clientLog.debug(`initial reconnect success: attempt=${attempt}, aid=${this._aid ?? ''}`);
3799
+ this._nextRetryAt = null;
3800
+ return;
3801
+ }
3447
3802
  if (this._sessionParams === null) {
3448
3803
  throw new StateError('missing connect params for reconnect');
3449
3804
  }
@@ -3471,7 +3826,9 @@ export class AUNClient {
3471
3826
  for (const gateway of candidates) {
3472
3827
  const attemptParams = { ...this._sessionParams, gateway };
3473
3828
  try {
3474
- await this._connectOnce(attemptParams, true);
3829
+ await this._connectOnce(attemptParams, true, reconnectAbort ?? undefined);
3830
+ if (!this._ownsReconnect(reconnectAbort))
3831
+ return;
3475
3832
  if (!this._transport.isConnected()) {
3476
3833
  throw new ConnectionError('websocket disconnected while reconnecting');
3477
3834
  }
@@ -3480,8 +3837,12 @@ export class AUNClient {
3480
3837
  break;
3481
3838
  }
3482
3839
  catch (exc) {
3840
+ if (!this._ownsReconnect(reconnectAbort))
3841
+ return;
3483
3842
  lastGatewayError = exc;
3484
3843
  await this._transport.close().catch(() => { });
3844
+ if (!this._ownsReconnect(reconnectAbort))
3845
+ return;
3485
3846
  if (candidates.length > 1) {
3486
3847
  this._clientLog.warn(`reconnect gateway failed, trying next: gateway=${gateway}, error=${formatCaughtError(exc)}`);
3487
3848
  }
@@ -3497,17 +3858,21 @@ export class AUNClient {
3497
3858
  return;
3498
3859
  }
3499
3860
  catch (exc) {
3500
- await this._dispatcher.publish('connection.error', {
3861
+ if (!this._ownsReconnect(reconnectAbort))
3862
+ return;
3863
+ if (!reconnectAbort || !await this._publishReconnectEvent(reconnectAbort, 'connection.error', {
3501
3864
  error: formatCaughtError(exc),
3502
3865
  attempt,
3503
- });
3866
+ }))
3867
+ return;
3504
3868
  if (!AUNClient._shouldRetryReconnect(exc)) {
3505
3869
  this._state = 'connection_failed';
3506
- await this._dispatcher.publish('state_change', {
3507
- state: this._publicState(this._state),
3508
- error: formatCaughtError(exc),
3509
- attempt,
3510
- });
3870
+ if (reconnectAbort)
3871
+ await this._publishReconnectEvent(reconnectAbort, 'state_change', {
3872
+ state: this._publicState(this._state),
3873
+ error: formatCaughtError(exc),
3874
+ attempt,
3875
+ });
3511
3876
  break;
3512
3877
  }
3513
3878
  delay = Math.min(delay * 2, maxBaseDelay);
@@ -3573,7 +3938,7 @@ export class AUNClient {
3573
3938
  || error instanceof ValidationError || error instanceof StateError) {
3574
3939
  return false;
3575
3940
  }
3576
- if (error instanceof ConnectionError)
3941
+ if (error instanceof ConnectionError || error instanceof TimeoutError)
3577
3942
  return true;
3578
3943
  if (error instanceof AUNError)
3579
3944
  return error.retryable;