@agentunion/fastaun-browser 0.5.6 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/_packed_docs/CHANGELOG.md +104 -0
  3. package/_packed_docs/INDEX.md +12 -11
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +4 -3
  5. package/_packed_docs/agent.md/SCHEMA.md +83 -58
  6. package/_packed_docs/agent.md/examples/codeagent-claudecode.md +1 -1
  7. package/_packed_docs/agent.md/examples/openclaw-lobster.md +1 -1
  8. package/_packed_docs/agent.md/examples/signed-openclaw-lobster.md +1 -1
  9. package/_packed_docs/agent.md/examples//347/276/244/347/273/204-/345/274/200/345/217/221/345/233/242/351/230/237.md +21 -0
  10. package/_packed_docs/audit/AUN/346/234/215/345/212/241Go/345/214/226/351/207/215/346/236/204/347/262/276/347/273/206/345/214/226/346/272/220/347/240/201/345/256/241/346/237/245-20260718.md +3 -3
  11. package/_packed_docs/aun/345/205/254/347/275/221/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +7 -0
  12. package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +422 -331
  13. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +598 -398
  14. package/_packed_docs/group-message-rpc-alignment-gaps.md +741 -0
  15. package/_packed_docs/message-online-push-alignment.md +572 -0
  16. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1 -1
  17. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +10 -10
  18. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +2 -2
  19. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +30 -10
  20. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +23 -20
  21. package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +3 -3
  22. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +5 -6
  23. package/_packed_docs/sdk/09-group-rpc-manual.md +11 -7
  24. package/_packed_docs/sdk/09-message-rpc-manual.md +5 -3
  25. package/_packed_docs/sdk/09-storage-rpc-manual.md +14 -3
  26. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +19 -17
  27. package/_packed_docs/sdk/CHANGELOG-0.5.6.md +73 -0
  28. package/_packed_docs/sdk/INDEX.md +22 -22
  29. package/_packed_docs/sdk/README.md +6 -5
  30. package/dist/agent-md-schema.d.ts +14 -0
  31. package/dist/agent-md-schema.d.ts.map +1 -0
  32. package/dist/agent-md-schema.js +103 -0
  33. package/dist/agent-md-schema.js.map +1 -0
  34. package/dist/agent-md.d.ts +7 -0
  35. package/dist/agent-md.d.ts.map +1 -1
  36. package/dist/agent-md.js +230 -10
  37. package/dist/agent-md.js.map +1 -1
  38. package/dist/aid-store.d.ts +5 -1
  39. package/dist/aid-store.d.ts.map +1 -1
  40. package/dist/aid-store.js +24 -4
  41. package/dist/aid-store.js.map +1 -1
  42. package/dist/aid.d.ts +2 -0
  43. package/dist/aid.d.ts.map +1 -1
  44. package/dist/aid.js +29 -7
  45. package/dist/aid.js.map +1 -1
  46. package/dist/auth.d.ts.map +1 -1
  47. package/dist/auth.js +1 -0
  48. package/dist/auth.js.map +1 -1
  49. package/dist/bundle.js +30253 -21955
  50. package/dist/cert-utils.d.ts +1 -0
  51. package/dist/cert-utils.d.ts.map +1 -1
  52. package/dist/cert-utils.js +36 -16
  53. package/dist/cert-utils.js.map +1 -1
  54. package/dist/client/delivery.d.ts +40 -5
  55. package/dist/client/delivery.d.ts.map +1 -1
  56. package/dist/client/delivery.js +719 -221
  57. package/dist/client/delivery.js.map +1 -1
  58. package/dist/client/group-state.js +2 -2
  59. package/dist/client/group-state.js.map +1 -1
  60. package/dist/client/lifecycle.d.ts +15 -1
  61. package/dist/client/lifecycle.d.ts.map +1 -1
  62. package/dist/client/lifecycle.js +463 -151
  63. package/dist/client/lifecycle.js.map +1 -1
  64. package/dist/client/rpc-pipeline.d.ts +25 -6
  65. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  66. package/dist/client/rpc-pipeline.js +407 -123
  67. package/dist/client/rpc-pipeline.js.map +1 -1
  68. package/dist/client/runtime.d.ts +1 -3
  69. package/dist/client/runtime.d.ts.map +1 -1
  70. package/dist/client/runtime.js +4 -7
  71. package/dist/client/runtime.js.map +1 -1
  72. package/dist/client/v2-e2ee.d.ts +20 -2
  73. package/dist/client/v2-e2ee.d.ts.map +1 -1
  74. package/dist/client/v2-e2ee.js +434 -112
  75. package/dist/client/v2-e2ee.js.map +1 -1
  76. package/dist/client.d.ts +22 -10
  77. package/dist/client.d.ts.map +1 -1
  78. package/dist/client.js +500 -116
  79. package/dist/client.js.map +1 -1
  80. package/dist/facades.d.ts +23 -1
  81. package/dist/facades.d.ts.map +1 -1
  82. package/dist/facades.js +55 -7
  83. package/dist/facades.js.map +1 -1
  84. package/dist/group-id.d.ts.map +1 -1
  85. package/dist/group-id.js +2 -17
  86. package/dist/group-id.js.map +1 -1
  87. package/dist/index.d.ts +4 -3
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +2 -1
  90. package/dist/index.js.map +1 -1
  91. package/dist/seq-tracker.d.ts.map +1 -1
  92. package/dist/seq-tracker.js +8 -2
  93. package/dist/seq-tracker.js.map +1 -1
  94. package/dist/tools/cross-sdk-agent.js +23 -1
  95. package/dist/tools/cross-sdk-agent.js.map +1 -1
  96. package/dist/transport.d.ts.map +1 -1
  97. package/dist/transport.js +8 -1
  98. package/dist/transport.js.map +1 -1
  99. package/dist/validators.d.ts.map +1 -1
  100. package/dist/validators.js +8 -14
  101. package/dist/validators.js.map +1 -1
  102. package/dist/version.d.ts +1 -1
  103. package/dist/version.js +1 -1
  104. package/package.json +6 -5
package/dist/client.js CHANGED
@@ -6,9 +6,9 @@
6
6
  // - 后台任务使用 setTimeout/setInterval
7
7
  import { createConfig, getDeviceId, normalizeSlotId } from './config.js';
8
8
  import { EventDispatcher } from './events.js';
9
+ import { normalizeGroupId } from './group-id.js';
9
10
  import { validateAIDFormat, validateGroupIDFormat } from './validators.js';
10
11
  import { GatewayDiscovery } from './discovery.js';
11
- import { AIDStore } from './aid-store.js';
12
12
  import { RPCTransport } from './transport.js';
13
13
  import { AuthFlow } from './auth.js';
14
14
  import { SeqTracker } from './seq-tracker.js';
@@ -19,7 +19,7 @@ import { IdentityRuntimeManager } from './client/identity.js';
19
19
  import { LifecycleController } from './client/lifecycle.js';
20
20
  import { PeerDirectory } from './client/peers.js';
21
21
  import { RpcPipeline } from './client/rpc-pipeline.js';
22
- import { GroupFacade, MessageFacade, StreamFacade } from './facades.js';
22
+ import { GroupFacade, MailFacade, MessageFacade, StreamFacade } from './facades.js';
23
23
  import { StorageVFS } from './storage/vfs.js';
24
24
  import { CollabClient } from './collab/client.js';
25
25
  import { V2E2EECoordinator } from './client/v2-e2ee.js';
@@ -29,7 +29,7 @@ import { IndexedDBTokenStore } from './keystore/indexeddb-token-store.js';
29
29
  import { decryptMessage, } from './v2/e2ee/index.js';
30
30
  import { ecdsaVerifyRaw } from './v2/crypto/ecdsa.js';
31
31
  import { AUNLogger } from './logger.js';
32
- import { AgentMdManager } from './agent-md.js';
32
+ import { AgentMdManager, buildDefaultAgentMd } from './agent-md.js';
33
33
  import { GroupIndexMetaCache } from './group-index.js';
34
34
  import { certMatchesFingerprint, normalizeFingerprintHex } from './cert-utils.js';
35
35
  import { AUNError, AuthError, ConnectionError, E2EEError, NotFoundError, PermissionError, StateError, TimeoutError, ValidationError, } from './errors.js';
@@ -99,7 +99,7 @@ const MAX_NOTIFY_PAYLOAD_SIZE = 64 * 1024;
99
99
  // P1-23: 非幂等方法使用更长超时(35s),避免 SDK 10s 超时 < gateway 30s 处理时间
100
100
  const NON_IDEMPOTENT_TIMEOUT = 35;
101
101
  const NON_IDEMPOTENT_METHODS = new Set([
102
- 'message.send', 'group.send', 'group.create', 'group.invite',
102
+ 'message.send', 'mail.send', 'group.send', 'group.create', 'group.invite',
103
103
  'group.kick', 'group.remove_member', 'group.leave', 'group.dissolve',
104
104
  'group.set_settings',
105
105
  'group.update_announcement', 'group.update_rules',
@@ -231,6 +231,7 @@ function createAgentMdManagerForRuntime(opts) {
231
231
  return new AgentMdManager({
232
232
  aunPath: opts.config().aunPath,
233
233
  tokenStore: opts.tokenStore(),
234
+ uploadViaGatewayHost: true,
234
235
  logger: opts.logger(),
235
236
  ownerAidGetter: opts.ownerAid,
236
237
  currentAidGetter: opts.currentAid,
@@ -561,6 +562,8 @@ export class AUNClient {
561
562
  _v2LazyProposeTriggered = new Map();
562
563
  /** agent.md 运行时管理器,负责上传、下载、缓存和 RPC 元数据观察。 */
563
564
  _agentMdManager;
565
+ _aidStore;
566
+ _groupIdentityOperations = new Map();
564
567
  _groupIndexMetaCache = new GroupIndexMetaCache();
565
568
  _groupIndexCacheLoaded = new Set();
566
569
  /** 消息序列号跟踪器(群消息 + P2P 空洞检测) */
@@ -580,12 +583,6 @@ export class AUNClient {
580
583
  _messageRecallSeen = new Map();
581
584
  /** 群撤回去重:group_id|sorted(message_ids) -> 时间戳,保证应用层只回调一次 */
582
585
  _groupRecallSeen = new Map();
583
- /** 在线未读 hint 队列:同一 group 只保留最后一条,延迟 drain 降低登录瞬时拉取压力。 */
584
- _onlineUnreadHintQueue = new Map();
585
- _onlineUnreadHintTimer = null;
586
- _onlineUnreadHintDrainActive = false;
587
- _onlineUnreadHintInitialDelayMs = 750;
588
- _onlineUnreadHintIntervalMs = 50;
589
586
  /** gap fill 来源标记:true 表示当前正在补洞(pull 触发),false 表示非补洞 */
590
587
  _gapFillActive = false;
591
588
  _backgroundRpcDepth = 0;
@@ -595,6 +592,8 @@ export class AUNClient {
595
592
  // 重连相关
596
593
  _reconnectActive = false;
597
594
  _reconnectAbort = null;
595
+ _reconnectTask = null;
596
+ _reconnectEventDispatchDepth = 0;
598
597
  _serverKicked = false;
599
598
  // 重连状态追踪(对齐 Python client.py)
600
599
  _nextRetryAt = null;
@@ -628,6 +627,7 @@ export class AUNClient {
628
627
  _storage;
629
628
  _collab;
630
629
  _messageFacade;
630
+ _mailFacade;
631
631
  _groupFacade;
632
632
  _streamFacade;
633
633
  constructor(aid) {
@@ -635,6 +635,7 @@ export class AUNClient {
635
635
  throw new ValidationError('AUNClient only accepts an AID object or no argument');
636
636
  }
637
637
  const inputAid = aid ?? null;
638
+ this._aidStore = inputAid?._aidStore;
638
639
  const rawConfig = {};
639
640
  if (inputAid)
640
641
  rawConfig.aun_path = inputAid.aunPath;
@@ -770,7 +771,7 @@ export class AUNClient {
770
771
  this._safeAsync(this._onGroupStateCommitted(data));
771
772
  });
772
773
  // 其他事件直接透传
773
- for (const evt of ['message.ack', 'storage.object_changed']) {
774
+ for (const evt of ['message.ack', 'storage.object_changed', 'stream/created', 'stream/closed']) {
774
775
  this._dispatcher.subscribe(`_raw.${evt}`, (data) => {
775
776
  this._dispatcher.publish(evt, data);
776
777
  });
@@ -784,6 +785,14 @@ export class AUNClient {
784
785
  get aid() {
785
786
  return this._aid;
786
787
  }
788
+ /** 返回后台刷新任务维护的最新 access token。 */
789
+ getAccessToken() {
790
+ const identityToken = String(this._identity?.access_token ?? '').trim();
791
+ if (identityToken)
792
+ return identityToken;
793
+ const sessionToken = String(this._sessionParams?.access_token ?? '').trim();
794
+ return sessionToken || null;
795
+ }
787
796
  /** AUN Storage VFS 入口 */
788
797
  get storage() {
789
798
  if (!this._storage)
@@ -849,6 +858,12 @@ export class AUNClient {
849
858
  }
850
859
  void this._persistGroupIndexCache(groupAid, fields);
851
860
  }
861
+ /** Mail bridge facade 入口 */
862
+ get mail() {
863
+ if (!this._mailFacade)
864
+ this._mailFacade = new MailFacade(this);
865
+ return this._mailFacade;
866
+ }
852
867
  getGroupIndexRemoteMeta(groupAid) {
853
868
  return this._groupIndexMetaCache.remoteMeta(this._aid ?? '', groupAid);
854
869
  }
@@ -1008,6 +1023,7 @@ export class AUNClient {
1008
1023
  this._v2PullPending = false;
1009
1024
  }
1010
1025
  _applyAidRuntimeContext(aid) {
1026
+ this._aidStore = aid._aidStore;
1011
1027
  const oldTransport = this._transport;
1012
1028
  try {
1013
1029
  const closeResult = oldTransport?.close?.();
@@ -1033,6 +1049,7 @@ export class AUNClient {
1033
1049
  this._peerCache.clear();
1034
1050
  this._certCache.clear();
1035
1051
  this._gatewayUrl = null;
1052
+ this._gatewayCandidates = [];
1036
1053
  this._deviceId = aid.deviceId || getDeviceId();
1037
1054
  this._slotId = aid.slotId || 'default';
1038
1055
  this._logger = new AUNLogger({ debug: aid.debug, aunPath: nextConfig.aunPath });
@@ -1173,16 +1190,126 @@ export class AUNClient {
1173
1190
  return;
1174
1191
  await this._handleTransportDisconnect(err);
1175
1192
  }
1176
- async createGroup(params = {}) {
1193
+ async _recoverIdentityAdmission(err, _method = '', params = {}) {
1194
+ if (!(err instanceof AUNError) || err.code !== -32003)
1195
+ return false;
1196
+ const message = String(err.message ?? '').trim();
1197
+ const groupCertPrefix = 'valid group identity certificate is required for ';
1198
+ if (message.toLowerCase().startsWith(groupCertPrefix)) {
1199
+ const fallback = message.slice(groupCertPrefix.length).trim();
1200
+ const store = this._aidStore;
1201
+ const groupId = String(params.group_id ?? params.groupId ?? params.group_aid ?? params.groupAid ?? fallback).trim();
1202
+ if (!groupId || !store)
1203
+ return false;
1204
+ try {
1205
+ await this._runGroupIdentityOperation(groupId, () => this.bindGroupAid({ group_id: groupId }, { aidStore: store }));
1206
+ }
1207
+ catch {
1208
+ return false;
1209
+ }
1210
+ return true;
1211
+ }
1212
+ const groupMDPrefix = 'valid group agent.md is required for ';
1213
+ if (message.toLowerCase().startsWith(groupMDPrefix)) {
1214
+ const groupAid = message.slice(groupMDPrefix.length).trim();
1215
+ const store = this._aidStore;
1216
+ if (!groupAid || !store)
1217
+ return false;
1218
+ const loaded = await store.load(groupAid);
1219
+ if (!loaded.ok || !loaded.data?.aid?.isPrivateKeyValid())
1220
+ return false;
1221
+ const ownerAid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
1222
+ if (!ownerAid)
1223
+ return false;
1224
+ await this._runGroupIdentityOperation(groupAid, () => this._uploadGroupAgentMd(store, groupAid, {}, {}, ownerAid));
1225
+ return true;
1226
+ }
1227
+ const agentPrefix = 'valid agent.md is required for ';
1228
+ if (!message.toLowerCase().startsWith(agentPrefix))
1229
+ return false;
1230
+ const target = message.slice(agentPrefix.length).trim();
1231
+ if (!target || target.toLowerCase() !== String(this._aid ?? '').trim().toLowerCase())
1232
+ return false;
1233
+ await this._agentMdManager.upload(await this._agentMdManager.readContent(target) ?? buildDefaultAgentMd(target));
1234
+ return true;
1235
+ }
1236
+ async _runGroupIdentityOperation(groupId, operation) {
1237
+ const aid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
1238
+ const dot = aid.indexOf('.');
1239
+ const key = normalizeGroupId(groupId, { localIssuer: dot >= 0 ? aid.slice(dot + 1) : '' }).toLowerCase();
1240
+ if (!key)
1241
+ return operation();
1242
+ let task = this._groupIdentityOperations.get(key);
1243
+ if (!task) {
1244
+ task = Promise.resolve().then(operation);
1245
+ this._groupIdentityOperations.set(key, task);
1246
+ }
1247
+ try {
1248
+ return await task;
1249
+ }
1250
+ finally {
1251
+ if (this._groupIdentityOperations.get(key) === task)
1252
+ this._groupIdentityOperations.delete(key);
1253
+ }
1254
+ }
1255
+ async createGroup(params = {}, options = {}) {
1177
1256
  if (!isJsonObject(params)) {
1178
1257
  throw new ValidationError('createGroup params must be an object');
1179
1258
  }
1259
+ const store = options.aidStore ?? this._aidStore;
1260
+ if (!store) {
1261
+ throw new ValidationError('createGroup requires aidStore');
1262
+ }
1263
+ this._aidStore = store;
1180
1264
  const payload = { ...params };
1181
- const groupName = String(payload.group_name ?? payload.groupName ?? '').trim();
1182
- if (!groupName) {
1183
- return await this.call('group.create', payload);
1265
+ if (payload.group_name === undefined && payload.groupName !== undefined) {
1266
+ payload.group_name = payload.groupName;
1267
+ delete payload.groupName;
1268
+ }
1269
+ const isNamed = Boolean(String(payload.group_name ?? '').trim());
1270
+ payload._defer_group_ready_postprocess = true;
1271
+ if (!isNamed) {
1272
+ delete payload.public_key;
1273
+ delete payload.curve;
1274
+ const created = await this.call('group.create', payload);
1275
+ const createdMap = isJsonObject(created) ? created : {};
1276
+ const createdGroup = isJsonObject(createdMap.group) ? createdMap.group : {};
1277
+ const groupId = String(createdGroup.group_id ?? createdGroup.group_aid ?? createdMap.group_id ?? createdMap.group_aid ?? '').trim();
1278
+ if (!groupId) {
1279
+ throw new ValidationError('group.create response missing group.group_id');
1280
+ }
1281
+ const bindParams = { group_id: groupId };
1282
+ for (const key of ['name', 'description', 'group_description', 'group_agent_md', 'groupAgentMd', 'content']) {
1283
+ if (key in payload)
1284
+ bindParams[key] = payload[key];
1285
+ }
1286
+ const bound = await this._runGroupIdentityOperation(groupId, () => this.bindGroupAid(bindParams, { aidStore: store }));
1287
+ const boundMap = isJsonObject(bound) ? bound : {};
1288
+ const boundGroup = isJsonObject(boundMap.group) ? boundMap.group : {};
1289
+ const result = { ...createdMap, ...boundMap, group: { ...createdGroup, ...boundGroup } };
1290
+ const postprocessParams = { ...payload };
1291
+ delete postprocessParams._defer_group_ready_postprocess;
1292
+ return await this._groupState.postprocessResult('group.create', postprocessParams, result);
1293
+ }
1294
+ const pendingKey = `create:${String(payload.group_name).trim().toLowerCase()}`;
1295
+ const keystore = store._keystore;
1296
+ let keyPair = null;
1297
+ if (keystore && typeof keystore.loadPendingGroupBind === 'function') {
1298
+ keyPair = await keystore.loadPendingGroupBind(pendingKey);
1299
+ }
1300
+ if (!keyPair || !keyPair.public_key_der_b64 || !keyPair.private_key_pem) {
1301
+ const generated = await new CryptoProvider().generateIdentity();
1302
+ keyPair = {
1303
+ private_key_pem: generated.private_key_pem,
1304
+ public_key_der_b64: generated.public_key_der_b64,
1305
+ curve: generated.curve,
1306
+ };
1307
+ if (keystore && typeof keystore.savePendingGroupBind === 'function') {
1308
+ await keystore.savePendingGroupBind(pendingKey, keyPair);
1309
+ }
1184
1310
  }
1185
- const keyPair = await new CryptoProvider().generateIdentity();
1311
+ if (!keyPair)
1312
+ throw new ValidationError('createGroup: failed to generate or load key pair');
1186
1313
  payload.public_key = keyPair.public_key_der_b64;
1187
1314
  payload.curve = keyPair.curve;
1188
1315
  const result = await this.call('group.create', payload);
@@ -1192,30 +1319,66 @@ export class AUNClient {
1192
1319
  const groupAid = String(group.group_aid ?? resultMap.group_aid ?? '').trim();
1193
1320
  const certPem = String(aidCert.cert ?? aidCert.cert_pem ?? resultMap.cert ?? resultMap.cert_pem ?? '').trim();
1194
1321
  if (!groupAid || !certPem) {
1195
- throw new ValidationError('group.create named group response missing group.group_aid or aid_cert.cert');
1322
+ throw new ValidationError('group.create response missing group.group_aid or aid_cert.cert');
1196
1323
  }
1197
- const store = new AIDStore({
1198
- aunPath: this.configModel.aunPath,
1199
- encryptionSeed: this.configModel.seedPassword ?? '',
1200
- slotId: this._slotId,
1201
- rootCaPem: this.configModel.rootCaPem,
1202
- verifySsl: this.configModel.verifySsl,
1324
+ const imported = await store.importGroupIdentity(groupAid, {
1325
+ private_key_pem: keyPair.private_key_pem,
1326
+ public_key_der_b64: keyPair.public_key_der_b64,
1327
+ curve: keyPair.curve,
1328
+ cert_pem: certPem,
1203
1329
  });
1204
- try {
1205
- const imported = await store.importGroupIdentity(groupAid, {
1206
- private_key_pem: keyPair.private_key_pem,
1207
- public_key_der_b64: keyPair.public_key_der_b64,
1208
- curve: keyPair.curve,
1209
- cert_pem: certPem,
1210
- });
1211
- if (!imported.ok) {
1212
- throw new ValidationError(imported.error.message);
1213
- }
1330
+ if (!imported.ok) {
1331
+ throw new ValidationError(imported.error.message);
1214
1332
  }
1215
- finally {
1216
- store.close();
1333
+ const uploaderAid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
1334
+ if (!uploaderAid) {
1335
+ throw new ValidationError('createGroup requires current owner AID for group agent.md upload');
1336
+ }
1337
+ await this._uploadGroupAgentMd(store, groupAid, payload, group, uploaderAid);
1338
+ if (keystore && typeof keystore.clearPendingGroupBind === 'function') {
1339
+ await keystore.clearPendingGroupBind(pendingKey);
1340
+ }
1341
+ const postprocessParams = { ...payload };
1342
+ delete postprocessParams._defer_group_ready_postprocess;
1343
+ return await this._groupState.postprocessResult('group.create', postprocessParams, result);
1344
+ }
1345
+ _groupAgentMdContent(groupAid, params = {}, group = {}) {
1346
+ const explicit = params.group_agent_md ?? params.groupAgentMd ?? params.content;
1347
+ if (explicit !== undefined && explicit !== null)
1348
+ return String(explicit);
1349
+ const value = (...keys) => {
1350
+ for (const source of [group, params]) {
1351
+ for (const key of keys) {
1352
+ const candidate = String(source[key] ?? '').trim();
1353
+ if (candidate)
1354
+ return candidate;
1355
+ }
1356
+ }
1357
+ return '';
1358
+ };
1359
+ const name = value('name', 'group_name', 'title') || groupAid;
1360
+ const normalizedDescription = (value('description', 'group_description') || `AUN group ${name}`)
1361
+ .split(/\s+/).filter(Boolean).join(' ');
1362
+ const description = Array.from(normalizedDescription).slice(0, 100).join('');
1363
+ return [
1364
+ '---\n',
1365
+ `aid: ${JSON.stringify(groupAid)}\n`,
1366
+ `name: ${JSON.stringify(name)}\n`,
1367
+ 'type: "group"\n',
1368
+ 'version: "1.0.0"\n',
1369
+ `description: ${JSON.stringify(description)}\n`,
1370
+ '---\n\n',
1371
+ `# ${name}\n`,
1372
+ ].join('');
1373
+ }
1374
+ async _uploadGroupAgentMd(store, groupAid, params, group, uploaderAid) {
1375
+ if (typeof store.uploadAgentMd !== 'function') {
1376
+ throw new ValidationError('group agent.md upload is unavailable');
1377
+ }
1378
+ const uploaded = await store.uploadAgentMd(groupAid, this._groupAgentMdContent(groupAid, params, group), { uploaderAid });
1379
+ if (!uploaded.ok) {
1380
+ throw new ValidationError(`group agent.md upload failed: ${uploaded.error.message}`);
1217
1381
  }
1218
- return result;
1219
1382
  }
1220
1383
  async bindGroupAid(params = {}, options = {}) {
1221
1384
  if (!isJsonObject(params)) {
@@ -1225,12 +1388,17 @@ export class AUNClient {
1225
1388
  if (!store) {
1226
1389
  throw new ValidationError('bindGroupAid requires aidStore');
1227
1390
  }
1391
+ this._aidStore = store;
1228
1392
  const groupId = String(params.group_id ?? params.group_aid ?? '').trim();
1393
+ const pendingKey = `bind:${groupId}`;
1229
1394
  const keystore = store._keystore;
1230
1395
  let keyPair = null;
1231
1396
  // 优先复用 pending 槽位中已暂存的待绑定密钥(崩溃/重试幂等)
1232
1397
  if (groupId && keystore && typeof keystore.loadPendingGroupBind === 'function') {
1233
- keyPair = await keystore.loadPendingGroupBind(groupId);
1398
+ keyPair = await keystore.loadPendingGroupBind(pendingKey);
1399
+ if (!keyPair || !keyPair.public_key_der_b64 || !keyPair.private_key_pem) {
1400
+ keyPair = await keystore.loadPendingGroupBind(groupId);
1401
+ }
1234
1402
  }
1235
1403
  if (!keyPair || !keyPair.public_key_der_b64 || !keyPair.private_key_pem) {
1236
1404
  // 未命中 pending,生成新密钥
@@ -1242,7 +1410,7 @@ export class AUNClient {
1242
1410
  };
1243
1411
  // 发 RPC 前先落盘暂存,确保崩溃后重试能复用同一密钥
1244
1412
  if (groupId && keystore && typeof keystore.savePendingGroupBind === 'function') {
1245
- await keystore.savePendingGroupBind(groupId, keyPair);
1413
+ await keystore.savePendingGroupBind(pendingKey, keyPair);
1246
1414
  }
1247
1415
  }
1248
1416
  if (!keyPair) {
@@ -1272,8 +1440,14 @@ export class AUNClient {
1272
1440
  if (!imported.ok) {
1273
1441
  throw new ValidationError(imported.error.message);
1274
1442
  }
1443
+ const uploaderAid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
1444
+ if (!uploaderAid) {
1445
+ throw new ValidationError('bindGroupAid requires current owner AID for group agent.md upload');
1446
+ }
1447
+ await this._uploadGroupAgentMd(store, groupAid, payload, group, uploaderAid);
1275
1448
  // 落盘成功,清除 pending 槽位
1276
1449
  if (groupId && keystore && typeof keystore.clearPendingGroupBind === 'function') {
1450
+ await keystore.clearPendingGroupBind(pendingKey);
1277
1451
  await keystore.clearPendingGroupBind(groupId);
1278
1452
  }
1279
1453
  return result;
@@ -1293,7 +1467,7 @@ export class AUNClient {
1293
1467
  let groupAid = String(params.group_aid ?? '').trim();
1294
1468
  if (!groupAid) {
1295
1469
  const info = await this.call('group.get_info', { group_id: groupId, required: ['member'] });
1296
- groupAid = String(info?.group_aid ?? '').trim();
1470
+ groupAid = String(info?.group_aid ?? info?.group?.group_aid ?? '').trim();
1297
1471
  }
1298
1472
  if (!groupAid) {
1299
1473
  throw new ValidationError('renewGroupAid: unable to determine group_aid');
@@ -1310,7 +1484,25 @@ export class AUNClient {
1310
1484
  if (!oldPublicKey) {
1311
1485
  throw new ValidationError(`renewGroupAid: cannot determine old public key for ${groupAid}`);
1312
1486
  }
1313
- const newKeyPair = await new CryptoProvider().generateIdentity();
1487
+ const keystore = store._keystore;
1488
+ const pendingKey = `renew:${groupId}`;
1489
+ let newKeyPair = null;
1490
+ if (keystore && typeof keystore.loadPendingGroupBind === 'function') {
1491
+ newKeyPair = await keystore.loadPendingGroupBind(pendingKey);
1492
+ }
1493
+ if (!newKeyPair || !newKeyPair.public_key_der_b64 || !newKeyPair.private_key_pem) {
1494
+ const generated = await new CryptoProvider().generateIdentity();
1495
+ newKeyPair = {
1496
+ private_key_pem: generated.private_key_pem,
1497
+ public_key_der_b64: generated.public_key_der_b64,
1498
+ curve: generated.curve,
1499
+ };
1500
+ if (keystore && typeof keystore.savePendingGroupBind === 'function') {
1501
+ await keystore.savePendingGroupBind(pendingKey, newKeyPair);
1502
+ }
1503
+ }
1504
+ if (!newKeyPair)
1505
+ throw new ValidationError('renewGroupAid: failed to generate or load key pair');
1314
1506
  const newPublicKey = newKeyPair.public_key_der_b64;
1315
1507
  const newPrivateKey = newKeyPair.private_key_pem;
1316
1508
  const curve = newKeyPair.curve || 'P-256';
@@ -1361,6 +1553,14 @@ export class AUNClient {
1361
1553
  if (!imported.ok) {
1362
1554
  throw new ValidationError(`renewGroupAid failed to persist group identity: ${imported.error.message}`);
1363
1555
  }
1556
+ const uploaderAid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
1557
+ if (!uploaderAid) {
1558
+ throw new ValidationError('renewGroupAid requires current owner AID for group agent.md upload');
1559
+ }
1560
+ await this._uploadGroupAgentMd(store, returnedGroupAid, payload, group, uploaderAid);
1561
+ if (keystore && typeof keystore.clearPendingGroupBind === 'function') {
1562
+ await keystore.clearPendingGroupBind(pendingKey);
1563
+ }
1364
1564
  return result;
1365
1565
  }
1366
1566
  async startGroupTransfer(params = {}, options = {}) {
@@ -1379,7 +1579,7 @@ export class AUNClient {
1379
1579
  let groupAid = String(params.group_aid ?? '').trim();
1380
1580
  if (!groupAid) {
1381
1581
  const info = await this.call('group.get_info', { group_id: groupId, required: ['member'] });
1382
- groupAid = String(info?.group_aid ?? '').trim();
1582
+ groupAid = String(info?.group_aid ?? info?.group?.group_aid ?? '').trim();
1383
1583
  }
1384
1584
  if (!groupAid) {
1385
1585
  throw new ValidationError('startGroupTransfer: unable to determine group_aid');
@@ -1418,20 +1618,38 @@ export class AUNClient {
1418
1618
  if (!store) {
1419
1619
  throw new ValidationError('completeGroupTransfer requires aidStore');
1420
1620
  }
1421
- const keyPair = await new CryptoProvider().generateIdentity();
1422
1621
  const payload = { ...params };
1423
1622
  const groupId = String(params.group_id ?? '').trim();
1424
1623
  if (!groupId) {
1425
1624
  throw new ValidationError('completeGroupTransfer requires group_id');
1426
1625
  }
1626
+ const pendingKey = `complete:${groupId}`;
1427
1627
  let groupAid = String(params.group_aid ?? '').trim();
1428
1628
  if (!groupAid) {
1429
1629
  const info = await this.call('group.get_info', { group_id: groupId, required: ['member'] });
1430
- groupAid = String(info?.group_aid ?? '').trim();
1630
+ groupAid = String(info?.group_aid ?? info?.group?.group_aid ?? '').trim();
1431
1631
  }
1432
1632
  if (!groupAid) {
1433
1633
  throw new ValidationError('completeGroupTransfer: unable to determine group_aid');
1434
1634
  }
1635
+ const keystore = store._keystore;
1636
+ let keyPair = null;
1637
+ if (keystore && typeof keystore.loadPendingGroupBind === 'function') {
1638
+ keyPair = await keystore.loadPendingGroupBind(pendingKey);
1639
+ }
1640
+ if (!keyPair || !keyPair.public_key_der_b64 || !keyPair.private_key_pem) {
1641
+ const generated = await new CryptoProvider().generateIdentity();
1642
+ keyPair = {
1643
+ private_key_pem: generated.private_key_pem,
1644
+ public_key_der_b64: generated.public_key_der_b64,
1645
+ curve: generated.curve,
1646
+ };
1647
+ if (keystore && typeof keystore.savePendingGroupBind === 'function') {
1648
+ await keystore.savePendingGroupBind(pendingKey, keyPair);
1649
+ }
1650
+ }
1651
+ if (!keyPair)
1652
+ throw new ValidationError('completeGroupTransfer: failed to generate or load key pair');
1435
1653
  const current = this.currentAid;
1436
1654
  const newOwner = String(current?.aid ?? '').trim();
1437
1655
  if (!current || !newOwner || !current.isPrivateKeyValid()) {
@@ -1475,6 +1693,14 @@ export class AUNClient {
1475
1693
  if (!imported.ok) {
1476
1694
  throw new ValidationError(imported.error.message);
1477
1695
  }
1696
+ const uploaderAid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
1697
+ if (!uploaderAid) {
1698
+ throw new ValidationError('completeGroupTransfer requires current owner AID for group agent.md upload');
1699
+ }
1700
+ await this._uploadGroupAgentMd(store, returnedGroupAid, payload, group, uploaderAid);
1701
+ if (keystore && typeof keystore.clearPendingGroupBind === 'function') {
1702
+ await keystore.clearPendingGroupBind(pendingKey);
1703
+ }
1478
1704
  return result;
1479
1705
  }
1480
1706
  static _notifyParamsSizeOk(params) {
@@ -1692,14 +1918,14 @@ export class AUNClient {
1692
1918
  const trace = `${this._echoTimestamp()} [AUN-SDK.receive] aid=${this._aid ?? '-'} conn_uptime=${uptime}s`;
1693
1919
  msg.payload = { ...payload, text: payload.text + '\n' + trace };
1694
1920
  }
1695
- async _drainOrderedMessages(ns, beforeSeq) {
1696
- await this._delivery.drainOrderedMessages(ns, beforeSeq);
1921
+ async _drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true) {
1922
+ await this._delivery.drainOrderedMessages(ns, beforeSeq, pullResponse, persist);
1697
1923
  }
1698
1924
  async _publishOrderedMessage(event, ns, seq, payload) {
1699
1925
  return this._delivery.publishOrderedMessage(event, ns, seq, payload);
1700
1926
  }
1701
- async _publishPulledMessage(event, ns, seq, payload) {
1702
- return this._delivery.publishPulledMessage(event, ns, seq, payload);
1927
+ async _publishPulledMessage(event, ns, seq, payload, persist = true) {
1928
+ return this._delivery.publishPulledMessage(event, ns, seq, payload, persist);
1703
1929
  }
1704
1930
  _extractGroupIdFromResult(result) {
1705
1931
  const group = isJsonObject(result.group) ? result.group : null;
@@ -1937,8 +2163,10 @@ export class AUNClient {
1937
2163
  await this._rpcPipeline.signClientOperation(method, params);
1938
2164
  }
1939
2165
  // ── 内部:连接 ────────────────────────────────────
1940
- async _connectOnce(params, allowReauth) {
2166
+ async _connectOnce(params, allowReauth, reconnectOwner, connectionOwner) {
1941
2167
  const tStart = Date.now();
2168
+ connectionOwner ??= this._connectAbort ?? undefined;
2169
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
1942
2170
  this._clientLog.debug(`_connectOnce enter: allow_reauth=${allowReauth}`);
1943
2171
  try {
1944
2172
  const gatewayUrl = this._resolveGateway(params);
@@ -1951,8 +2179,12 @@ export class AUNClient {
1951
2179
  // 避免 reader 把积压 push 交给空 tracker 的 handler,触发 S2 历史 gap 误补拉。
1952
2180
  this._refreshSeqTrackerContext();
1953
2181
  await this._restoreSeqTrackerState();
2182
+ this._assertReconnectOwner(reconnectOwner);
2183
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
1954
2184
  try {
1955
2185
  const challenge = await this._transport.connect(gatewayUrl);
2186
+ this._assertReconnectOwner(reconnectOwner);
2187
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
1956
2188
  this._state = 'authenticating';
1957
2189
  if (allowReauth) {
1958
2190
  const authContext = await this._auth.connectSession(this._transport, challenge, gatewayUrl, {
@@ -1964,6 +2196,8 @@ export class AUNClient {
1964
2196
  shortTtlMs: Number(params.short_ttl_ms ?? 0),
1965
2197
  extraInfo: params.extra_info,
1966
2198
  });
2199
+ this._assertReconnectOwner(reconnectOwner);
2200
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
1967
2201
  if (isJsonObject(authContext)) {
1968
2202
  const auth = authContext;
1969
2203
  const identity = auth.identity;
@@ -1988,7 +2222,10 @@ export class AUNClient {
1988
2222
  shortTtlMs: Number(params.short_ttl_ms ?? 0),
1989
2223
  extraInfo: params.extra_info,
1990
2224
  });
2225
+ this._assertReconnectOwner(reconnectOwner);
2226
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
1991
2227
  await this._syncIdentityAfterConnect(String(params.access_token));
2228
+ this._assertReconnectOwner(reconnectOwner);
1992
2229
  if (isJsonObject(hello) && 'heartbeat_interval' in hello) {
1993
2230
  this._applyServerHeartbeatInterval(hello.heartbeat_interval, 'auth');
1994
2231
  }
@@ -1996,13 +2233,19 @@ export class AUNClient {
1996
2233
  }
1997
2234
  catch (err) {
1998
2235
  // P1-19: 首连失败时重置状态,避免半连接残留
1999
- this._state = 'standby';
2236
+ if ((!reconnectOwner || this._ownsReconnect(reconnectOwner))
2237
+ && (!connectionOwner || this._lifecycle.ownsConnectionAttempt(connectionOwner))
2238
+ && !this._closing) {
2239
+ this._state = 'standby';
2240
+ }
2000
2241
  try {
2001
2242
  await this._transport.close();
2002
2243
  }
2003
2244
  catch { /* 忽略关闭错误 */ }
2004
2245
  throw err;
2005
2246
  }
2247
+ this._assertReconnectOwner(reconnectOwner);
2248
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
2006
2249
  if (!this._transport.isConnected()) {
2007
2250
  throw new ConnectionError('websocket disconnected during authentication');
2008
2251
  }
@@ -2017,14 +2260,30 @@ export class AUNClient {
2017
2260
  else if (gatewayUrl) {
2018
2261
  this._gatewayCandidates = [gatewayUrl];
2019
2262
  }
2020
- await this._dispatcher.publish('state_change', {
2263
+ const statePayload = {
2021
2264
  state: this._publicState(this._state),
2022
2265
  gateway: gatewayUrl,
2023
- });
2266
+ };
2267
+ if (reconnectOwner) {
2268
+ if (!await this._publishReconnectEvent(reconnectOwner, 'state_change', statePayload)) {
2269
+ throw new ConnectionError('reconnect attempt superseded');
2270
+ }
2271
+ }
2272
+ else if (connectionOwner) {
2273
+ if (!await this._lifecycle.publishConnectionEvent(connectionOwner, 'state_change', statePayload)) {
2274
+ throw new StateError('connection attempt superseded');
2275
+ }
2276
+ }
2277
+ else {
2278
+ await this._dispatcher.publish('state_change', statePayload);
2279
+ }
2280
+ this._assertReconnectOwner(reconnectOwner);
2281
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
2024
2282
  if (this._seqTrackerContext !== this._currentSeqTrackerContext()) {
2025
2283
  this._refreshSeqTrackerContext();
2026
2284
  await this._restoreSeqTrackerState();
2027
2285
  }
2286
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
2028
2287
  this._startBackgroundTasks();
2029
2288
  const connectionKind = String(params.connection_kind ?? 'long');
2030
2289
  const isShortConnection = connectionKind === 'short';
@@ -2033,6 +2292,8 @@ export class AUNClient {
2033
2292
  && (!isShortConnection || hasExplicitBackgroundSync);
2034
2293
  if (!isShortConnection) {
2035
2294
  await this._v2E2EE.onConnected({ backgroundSync: backgroundSyncEnabled });
2295
+ this._assertReconnectOwner(reconnectOwner);
2296
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
2036
2297
  }
2037
2298
  else {
2038
2299
  this._clientLog.debug('V2 session init deferred for short connection');
@@ -2063,6 +2324,11 @@ export class AUNClient {
2063
2324
  const target = String(aid ?? this._aid ?? '').trim();
2064
2325
  if (!target)
2065
2326
  throw new StateError('gateway discovery requires a loaded AID');
2327
+ const discovery = this._discovery;
2328
+ const tokenStore = this._tokenStore;
2329
+ const discoveryIsCurrent = () => (this._discovery === discovery
2330
+ && this._tokenStore === tokenStore
2331
+ && !this._closing);
2066
2332
  if (this._gatewayCandidates.length > 0)
2067
2333
  return [...this._gatewayCandidates];
2068
2334
  if (this._gatewayUrl) {
@@ -2070,14 +2336,18 @@ export class AUNClient {
2070
2336
  return [...this._gatewayCandidates];
2071
2337
  }
2072
2338
  try {
2073
- const getMetadata = this._tokenStore.getMetadata;
2339
+ const getMetadata = tokenStore.getMetadata;
2074
2340
  const rawList = typeof getMetadata === 'function'
2075
- ? String(await getMetadata.call(this._tokenStore, target, 'gateway_urls') ?? '').trim()
2341
+ ? String(await getMetadata.call(tokenStore, target, 'gateway_urls') ?? '').trim()
2076
2342
  : '';
2343
+ if (!discoveryIsCurrent())
2344
+ throw new StateError('gateway discovery superseded by identity change');
2077
2345
  const cachedList = rawList ? JSON.parse(rawList) : [];
2078
2346
  const raw = typeof getMetadata === 'function'
2079
- ? String(await getMetadata.call(this._tokenStore, target, 'gateway_url') ?? '').trim()
2347
+ ? String(await getMetadata.call(tokenStore, target, 'gateway_url') ?? '').trim()
2080
2348
  : '';
2349
+ if (!discoveryIsCurrent())
2350
+ throw new StateError('gateway discovery superseded by identity change');
2081
2351
  const candidates = Array.isArray(cachedList)
2082
2352
  ? cachedList.map((item) => String(item ?? '').trim()).filter(Boolean)
2083
2353
  : [];
@@ -2089,6 +2359,8 @@ export class AUNClient {
2089
2359
  }
2090
2360
  }
2091
2361
  catch {
2362
+ if (!discoveryIsCurrent())
2363
+ throw new StateError('gateway discovery superseded by identity change');
2092
2364
  // 缓存读取失败不影响发现流程。
2093
2365
  }
2094
2366
  const dotIdx = target.indexOf('.');
@@ -2101,7 +2373,9 @@ export class AUNClient {
2101
2373
  let lastError = null;
2102
2374
  for (const url of candidates) {
2103
2375
  try {
2104
- const discovered = await this._discovery.discoverAll(url);
2376
+ const discovered = await discovery.discoverAll(url);
2377
+ if (!discoveryIsCurrent())
2378
+ throw new StateError('gateway discovery superseded by identity change');
2105
2379
  const gatewayUrls = [...new Set(discovered.map((item) => String(item ?? '').trim()).filter(Boolean))];
2106
2380
  if (gatewayUrls.length === 0)
2107
2381
  throw new ValidationError('gateway discovery returned no candidates');
@@ -2109,10 +2383,14 @@ export class AUNClient {
2109
2383
  this._gatewayCandidates = gatewayUrls;
2110
2384
  this._gatewayUrl = gateway;
2111
2385
  try {
2112
- const setMetadata = this._tokenStore.setMetadata;
2386
+ const setMetadata = tokenStore.setMetadata;
2113
2387
  if (typeof setMetadata === 'function') {
2114
- await setMetadata.call(this._tokenStore, target, 'gateway_url', gateway);
2115
- await setMetadata.call(this._tokenStore, target, 'gateway_urls', JSON.stringify(gatewayUrls));
2388
+ await setMetadata.call(tokenStore, target, 'gateway_url', gateway);
2389
+ if (!discoveryIsCurrent())
2390
+ throw new StateError('gateway discovery superseded by identity change');
2391
+ await setMetadata.call(tokenStore, target, 'gateway_urls', JSON.stringify(gatewayUrls));
2392
+ if (!discoveryIsCurrent())
2393
+ throw new StateError('gateway discovery superseded by identity change');
2116
2394
  }
2117
2395
  }
2118
2396
  catch {
@@ -2121,6 +2399,8 @@ export class AUNClient {
2121
2399
  return [...gatewayUrls];
2122
2400
  }
2123
2401
  catch (err) {
2402
+ if (!discoveryIsCurrent())
2403
+ throw new StateError('gateway discovery superseded by identity change');
2124
2404
  lastError = err;
2125
2405
  }
2126
2406
  }
@@ -2442,13 +2722,16 @@ export class AUNClient {
2442
2722
  this._clientLog.debug(`publish gateway.disconnect failed: ${exc?.message ?? exc}`);
2443
2723
  }
2444
2724
  }
2445
- async _handleTransportDisconnect(error, closeCode) {
2725
+ async _handleTransportDisconnect(error, closeCode, initialOptions) {
2446
2726
  if (this._closing || this._state === 'closed')
2447
2727
  return;
2448
2728
  // 在首个 await 前占位,避免 close/error 与 token 刷新并发启动多条重连链。
2449
2729
  if (this._reconnectActive)
2450
2730
  return;
2451
2731
  this._reconnectActive = true;
2732
+ // 在首个 await 前创建取消控制器,覆盖 disconnect 与清理动作的竞态窗口。
2733
+ const reconnectAbort = new AbortController();
2734
+ this._reconnectAbort = reconnectAbort;
2452
2735
  // 不等待状态事件或 transport 清理,先隔离已在途的 inline Push 回调。
2453
2736
  this._delivery.resetInlineAckState();
2454
2737
  this._state = 'standby';
@@ -2464,8 +2747,18 @@ export class AUNClient {
2464
2747
  state: this._publicState(this._state),
2465
2748
  error,
2466
2749
  });
2750
+ if (reconnectAbort.signal.aborted || this._reconnectAbort !== reconnectAbort || this._closing) {
2751
+ if (this._reconnectAbort === reconnectAbort) {
2752
+ this._reconnectAbort = null;
2753
+ this._reconnectActive = false;
2754
+ }
2755
+ return;
2756
+ }
2467
2757
  if (!this._sessionOptions.auto_reconnect) {
2468
- this._reconnectActive = false;
2758
+ if (this._reconnectAbort === reconnectAbort) {
2759
+ this._reconnectAbort = null;
2760
+ this._reconnectActive = false;
2761
+ }
2469
2762
  return;
2470
2763
  }
2471
2764
  // 不重连 close code(认证失败/权限错误/被踢等)或服务端通知断开:抑制重连
@@ -2487,30 +2780,90 @@ export class AUNClient {
2487
2780
  eventPayload.code = disconnectInfo.code;
2488
2781
  }
2489
2782
  await this._dispatcher.publish('state_change', eventPayload);
2490
- this._reconnectActive = false;
2783
+ if (this._reconnectAbort === reconnectAbort) {
2784
+ this._reconnectAbort = null;
2785
+ this._reconnectActive = false;
2786
+ }
2491
2787
  return;
2492
2788
  }
2493
2789
  // 1000 = 正常关闭, 1006 = 网络异常断开(无 close frame),其他 code = 服务端主动关闭
2494
2790
  const serverInitiated = closeCode !== undefined && closeCode !== 1000 && closeCode !== 1006;
2495
- if (this._closing) {
2496
- this._reconnectActive = false;
2791
+ if (this._closing || reconnectAbort.signal.aborted || this._reconnectAbort !== reconnectAbort) {
2792
+ if (this._reconnectAbort === reconnectAbort) {
2793
+ this._reconnectAbort = null;
2794
+ this._reconnectActive = false;
2795
+ }
2497
2796
  return;
2498
2797
  }
2499
- const reconnectAbort = new AbortController();
2500
- this._reconnectAbort = reconnectAbort;
2501
- this._safeAsync(this._reconnectLoop(serverInitiated, reconnectAbort));
2798
+ const reconnectTask = Promise.resolve().then(() => this._reconnectLoop(serverInitiated, reconnectAbort, initialOptions));
2799
+ this._reconnectTask = reconnectTask;
2800
+ this._safeAsync(reconnectTask);
2801
+ }
2802
+ _ownsReconnect(owner) {
2803
+ return Boolean(owner
2804
+ && !owner.signal.aborted
2805
+ && !this._closing
2806
+ && this._reconnectAbort === owner);
2807
+ }
2808
+ _assertReconnectOwner(owner) {
2809
+ if (owner && !this._ownsReconnect(owner)) {
2810
+ throw new ConnectionError('reconnect attempt superseded');
2811
+ }
2812
+ }
2813
+ async _publishReconnectEvent(owner, event, payload) {
2814
+ if (!this._ownsReconnect(owner))
2815
+ return false;
2816
+ this._reconnectEventDispatchDepth += 1;
2817
+ try {
2818
+ await this._dispatcher.publish(event, payload);
2819
+ }
2820
+ finally {
2821
+ this._reconnectEventDispatchDepth -= 1;
2822
+ }
2823
+ return this._ownsReconnect(owner);
2824
+ }
2825
+ async _cancelReconnectAndWait() {
2826
+ const owner = this._reconnectAbort;
2827
+ const task = this._reconnectTask;
2828
+ if (!owner && !task && !this._reconnectActive)
2829
+ return;
2830
+ owner?.abort();
2831
+ try {
2832
+ await this._transport.close();
2833
+ }
2834
+ catch (exc) {
2835
+ this._clientLog.debug(`reconnect cancellation transport cleanup skipped: ${formatCaughtError(exc)}`);
2836
+ }
2837
+ // state_change handler 内等待其所属重连任务会自锁;该路径由事件返回后的
2838
+ // owner 复核负责收尾,且事件回调期间尚未进入下一次 transport 操作。
2839
+ if (task && this._reconnectEventDispatchDepth === 0) {
2840
+ try {
2841
+ await task;
2842
+ }
2843
+ catch (exc) {
2844
+ this._clientLog.debug(`cancelled reconnect task exited with error: ${formatCaughtError(exc)}`);
2845
+ }
2846
+ }
2847
+ if (this._reconnectAbort === owner) {
2848
+ this._reconnectAbort = null;
2849
+ this._reconnectActive = false;
2850
+ this._nextRetryAt = null;
2851
+ }
2852
+ if (this._reconnectTask === task)
2853
+ this._reconnectTask = null;
2502
2854
  }
2503
2855
  /** 重连循环入口;无论正常退出还是意外异常,都只清理自己这一代控制器。 */
2504
- async _reconnectLoop(serverInitiated = false, reconnectAbort = this._reconnectAbort) {
2856
+ async _reconnectLoop(serverInitiated = false, reconnectAbort = this._reconnectAbort, initialOptions) {
2505
2857
  let releasedOwnGeneration = false;
2506
2858
  try {
2507
- await this._runReconnectLoop(serverInitiated, reconnectAbort);
2859
+ await this._runReconnectLoop(serverInitiated, reconnectAbort, initialOptions);
2508
2860
  }
2509
2861
  finally {
2510
2862
  if (this._reconnectAbort === reconnectAbort) {
2511
2863
  this._nextRetryAt = null;
2512
2864
  this._reconnectActive = false;
2513
2865
  this._reconnectAbort = null;
2866
+ this._reconnectTask = null;
2514
2867
  releasedOwnGeneration = true;
2515
2868
  }
2516
2869
  }
@@ -2519,7 +2872,7 @@ export class AUNClient {
2519
2872
  }
2520
2873
  }
2521
2874
  /** 指数退避 + 当前基数抖动重连循环主体。 */
2522
- async _runReconnectLoop(serverInitiated, reconnectAbort) {
2875
+ async _runReconnectLoop(serverInitiated, reconnectAbort, initialOptions) {
2523
2876
  const retry = { ...this._sessionOptions.retry };
2524
2877
  const maxBaseDelay = clampReconnectDelaySeconds(retry.max_delay, RECONNECT_MAX_BASE_DELAY_SECONDS);
2525
2878
  // M25: max_attempts=0 表示无限重试(与 Go/Python 对齐)
@@ -2534,11 +2887,12 @@ export class AUNClient {
2534
2887
  if (maxAttempts > 0 && attempt > maxAttempts) {
2535
2888
  this._state = 'terminal_failed';
2536
2889
  this._nextRetryAt = null;
2537
- await this._dispatcher.publish('state_change', {
2538
- state: this._publicState(this._state),
2539
- attempt: attempt - 1,
2540
- reason: 'max_attempts_exhausted',
2541
- });
2890
+ if (reconnectAbort)
2891
+ await this._publishReconnectEvent(reconnectAbort, 'state_change', {
2892
+ state: this._publicState(this._state),
2893
+ attempt: attempt - 1,
2894
+ reason: 'max_attempts_exhausted',
2895
+ });
2542
2896
  return;
2543
2897
  }
2544
2898
  // 先进入 retry_backoff 状态(对齐 Python:先退避再重连)
@@ -2546,22 +2900,26 @@ export class AUNClient {
2546
2900
  const sleepMs = reconnectSleepDelaySeconds(delay, maxBaseDelay) * 1000;
2547
2901
  this._nextRetryAt = new Date(Date.now() + sleepMs);
2548
2902
  this._state = 'retry_backoff';
2549
- await this._dispatcher.publish('state_change', {
2903
+ if (!reconnectAbort || !await this._publishReconnectEvent(reconnectAbort, 'state_change', {
2550
2904
  state: this._publicState(this._state),
2551
2905
  attempt,
2552
2906
  next_retry_at: this._nextRetryAt.getTime() / 1000,
2553
- });
2907
+ }))
2908
+ return;
2554
2909
  try {
2555
2910
  await this._sleep(sleepMs, reconnectAbort?.signal);
2556
2911
  this._nextRetryAt = null;
2557
- if (reconnectAbort?.signal.aborted)
2912
+ if (!this._ownsReconnect(reconnectAbort))
2558
2913
  return;
2559
2914
  // 退避结束,进入 reconnecting 状态
2560
2915
  this._state = 'reconnecting';
2561
- await this._dispatcher.publish('state_change', {
2916
+ if (!reconnectAbort || !await this._publishReconnectEvent(reconnectAbort, 'state_change', {
2562
2917
  state: this._publicState(this._state),
2563
2918
  attempt,
2564
- });
2919
+ }))
2920
+ return;
2921
+ if (!this._ownsReconnect(reconnectAbort))
2922
+ return;
2565
2923
  // /health 仅作旁路诊断,不能阻止实际 WebSocket 重连。
2566
2924
  if (this._gatewayUrl) {
2567
2925
  void this._discovery.checkHealth(this._gatewayUrl, 5000)
@@ -2574,6 +2932,20 @@ export class AUNClient {
2574
2932
  });
2575
2933
  }
2576
2934
  await this._transport.close();
2935
+ if (!this._ownsReconnect(reconnectAbort))
2936
+ return;
2937
+ if (initialOptions) {
2938
+ // 首连失败时没有可复用的 session/gateway,必须重新 discovery + authenticate。
2939
+ await this._lifecycle.retryInitialConnect(initialOptions, reconnectAbort ?? undefined);
2940
+ if (!this._ownsReconnect(reconnectAbort))
2941
+ return;
2942
+ if (!this._transport.isConnected()) {
2943
+ throw new ConnectionError('websocket disconnected while initial reconnect completed');
2944
+ }
2945
+ this._clientLog.debug(`initial reconnect success: attempt=${attempt}, aid=${this._aid ?? ''}`);
2946
+ this._nextRetryAt = null;
2947
+ return;
2948
+ }
2577
2949
  if (!this._sessionParams) {
2578
2950
  throw new StateError('missing connect params for reconnect');
2579
2951
  }
@@ -2601,7 +2973,9 @@ export class AUNClient {
2601
2973
  for (const gateway of candidates) {
2602
2974
  const attemptParams = { ...this._sessionParams, gateway };
2603
2975
  try {
2604
- await this._connectOnce(attemptParams, true);
2976
+ await this._connectOnce(attemptParams, true, reconnectAbort ?? undefined);
2977
+ if (!this._ownsReconnect(reconnectAbort))
2978
+ return;
2605
2979
  if (!this._transport.isConnected()) {
2606
2980
  throw new ConnectionError('websocket disconnected while reconnecting');
2607
2981
  }
@@ -2627,20 +3001,24 @@ export class AUNClient {
2627
3001
  return;
2628
3002
  }
2629
3003
  catch (exc) {
3004
+ if (!this._ownsReconnect(reconnectAbort))
3005
+ return;
2630
3006
  this._lastError = exc instanceof Error ? exc : new Error(String(exc));
2631
3007
  this._lastErrorCode = 'reconnect_failed';
2632
- await this._dispatcher.publish('connection.error', {
3008
+ if (!reconnectAbort || !await this._publishReconnectEvent(reconnectAbort, 'connection.error', {
2633
3009
  error: formatCaughtError(exc),
2634
3010
  attempt,
2635
- });
3011
+ }))
3012
+ return;
2636
3013
  if (!this._shouldRetryReconnect(exc)) {
2637
3014
  this._state = 'terminal_failed';
2638
3015
  this._nextRetryAt = null;
2639
- await this._dispatcher.publish('state_change', {
2640
- state: this._publicState(this._state),
2641
- error: formatCaughtError(exc),
2642
- attempt,
2643
- });
3016
+ if (reconnectAbort)
3017
+ await this._publishReconnectEvent(reconnectAbort, 'state_change', {
3018
+ state: this._publicState(this._state),
3019
+ error: formatCaughtError(exc),
3020
+ attempt,
3021
+ });
2644
3022
  return;
2645
3023
  }
2646
3024
  delay = Math.min(delay * 2, maxBaseDelay);
@@ -2663,7 +3041,7 @@ export class AUNClient {
2663
3041
  || error instanceof ValidationError || error instanceof StateError) {
2664
3042
  return false;
2665
3043
  }
2666
- if (error instanceof ConnectionError)
3044
+ if (error instanceof ConnectionError || error instanceof TimeoutError)
2667
3045
  return true;
2668
3046
  if (error instanceof AUNError)
2669
3047
  return error.retryable;
@@ -2683,6 +3061,7 @@ export class AUNClient {
2683
3061
  _resetSeqTrackingState() {
2684
3062
  void this._saveSeqTrackerState();
2685
3063
  this._resetV2IdentityRuntime();
3064
+ this._v2E2EE.resetForwardCursorState();
2686
3065
  this._seqTracker = new SeqTracker();
2687
3066
  this._seqTrackerContext = null;
2688
3067
  this._gapFillDone.clear();
@@ -2692,18 +3071,13 @@ export class AUNClient {
2692
3071
  this._v2SenderIKPending.clear();
2693
3072
  this._v2SenderIKFetching.clear();
2694
3073
  this._groupSynced.clear();
2695
- this._onlineUnreadHintQueue.clear();
2696
- if (this._onlineUnreadHintTimer) {
2697
- clearTimeout(this._onlineUnreadHintTimer);
2698
- this._onlineUnreadHintTimer = null;
2699
- }
2700
- this._onlineUnreadHintDrainActive = false;
2701
3074
  }
2702
3075
  _refreshSeqTrackerContext() {
2703
3076
  const nextContext = this._currentSeqTrackerContext();
2704
3077
  if (nextContext === this._seqTrackerContext)
2705
3078
  return;
2706
3079
  void this._saveSeqTrackerState();
3080
+ this._v2E2EE.resetForwardCursorState();
2707
3081
  this._seqTracker = new SeqTracker();
2708
3082
  this._delivery.resetInlineAckState();
2709
3083
  this._gapFillDone.clear();
@@ -2713,12 +3087,6 @@ export class AUNClient {
2713
3087
  this._v2SenderIKPending.clear();
2714
3088
  this._v2SenderIKFetching.clear();
2715
3089
  this._groupSynced.clear();
2716
- this._onlineUnreadHintQueue.clear();
2717
- if (this._onlineUnreadHintTimer) {
2718
- clearTimeout(this._onlineUnreadHintTimer);
2719
- this._onlineUnreadHintTimer = null;
2720
- }
2721
- this._onlineUnreadHintDrainActive = false;
2722
3090
  this._seqTrackerContext = nextContext;
2723
3091
  }
2724
3092
  /** 将 SeqTracker 状态保存到 keystore */
@@ -2941,7 +3309,7 @@ export class AUNClient {
2941
3309
  * undecryptable 事件、成功消费后触发 SPK 轮换。inline Push 在游标提交和
2942
3310
  * Head/正文绑定校验前调用本入口时必须显式关闭这些副作用。
2943
3311
  */
2944
- async _decryptV2Message(msg, allowPending = true, emitUndecryptable = true, rotateKeys = true, observeAgentMd = true) {
3312
+ async _decryptV2Message(msg, allowPending = true, emitUndecryptable = true, rotateKeys = true, observeAgentMd = true, deferStatus, deferKeyFetch = false) {
2945
3313
  const session = this._v2Session;
2946
3314
  if (!session)
2947
3315
  return null;
@@ -3008,6 +3376,24 @@ export class AUNClient {
3008
3376
  }
3009
3377
  catch (exc) {
3010
3378
  this._clientLog.warn(`V2 decrypt: SPK lookup failed seq=${String(msg.seq)} spk_id=${spkId}: ${String(exc)}`);
3379
+ if (allowPending || deferStatus) {
3380
+ const fromAid = String(msg.from_aid ?? '');
3381
+ const senderDeviceId = String(aad.from_device ?? '');
3382
+ if (deferStatus) {
3383
+ deferStatus.deferred = true;
3384
+ deferStatus.fromAid = fromAid;
3385
+ deferStatus.senderDeviceId = senderDeviceId;
3386
+ deferStatus.groupId = groupIdForKeys;
3387
+ }
3388
+ this._scheduleV2SenderIKPending({
3389
+ msg,
3390
+ fromAid,
3391
+ senderDeviceId,
3392
+ groupId: groupIdForKeys,
3393
+ startFetch: !deferKeyFetch,
3394
+ });
3395
+ return null;
3396
+ }
3011
3397
  if (emitUndecryptable) {
3012
3398
  try {
3013
3399
  const event = {
@@ -3035,13 +3421,19 @@ export class AUNClient {
3035
3421
  const senderDeviceId = String(aad.from_device ?? '');
3036
3422
  const senderCertFingerprint = String(envelope.sender_cert_fingerprint ?? '').trim().toLowerCase();
3037
3423
  let senderPubDer = await this._getV2SenderPubDer(fromAid, senderDeviceId, senderCertFingerprint);
3038
- if (!senderPubDer) {
3424
+ if (!senderPubDer && !deferKeyFetch) {
3039
3425
  this._clientLog.warn(`V2 decrypt: no sender IK for ${fromAid} device=${senderDeviceId}`);
3040
3426
  senderPubDer = await this._fetchV2SenderIKOnce(fromAid, senderDeviceId, groupIdForKeys, senderCertFingerprint);
3041
3427
  }
3042
3428
  if (!senderPubDer) {
3043
- if (allowPending) {
3044
- this._scheduleV2SenderIKPending({ msg, fromAid, senderDeviceId, groupId: groupIdForKeys });
3429
+ if (allowPending || deferStatus) {
3430
+ if (deferStatus) {
3431
+ deferStatus.deferred = true;
3432
+ deferStatus.fromAid = fromAid;
3433
+ deferStatus.senderDeviceId = senderDeviceId;
3434
+ deferStatus.groupId = groupIdForKeys;
3435
+ }
3436
+ this._scheduleV2SenderIKPending({ msg, fromAid, senderDeviceId, groupId: groupIdForKeys, startFetch: !deferKeyFetch });
3045
3437
  return null;
3046
3438
  }
3047
3439
  if (emitUndecryptable) {
@@ -3069,9 +3461,13 @@ export class AUNClient {
3069
3461
  let plaintext;
3070
3462
  try {
3071
3463
  plaintext = await decryptMessage(envelope, this._aid ?? '', this._deviceId, ikPriv, spkPriv, senderPubDer);
3464
+ if (plaintext === null)
3465
+ throw new Error('recipient_not_found_or_envelope_verification_failed');
3072
3466
  }
3073
3467
  catch (exc) {
3074
3468
  this._clientLog.warn(`V2 decrypt failed for msg seq=${String(msg.seq)}: ${String(exc)}`);
3469
+ if (deferStatus)
3470
+ deferStatus.terminal = true;
3075
3471
  if (emitUndecryptable) {
3076
3472
  try {
3077
3473
  const event = {
@@ -3321,18 +3717,6 @@ export class AUNClient {
3321
3717
  this._clientLog.warn(`background task exception:${String(exc)}`);
3322
3718
  });
3323
3719
  }
3324
- _explicitGroupCursorParams(params) {
3325
- const value = params._group_cursor_params;
3326
- if (!isJsonObject(value))
3327
- return {};
3328
- return { ...value };
3329
- }
3330
- _groupCursorTargetsCurrentInstance(params) {
3331
- const deviceId = String(params.device_id ?? '').trim();
3332
- const slotId = String(params.slot_id ?? '').trim();
3333
- return (!deviceId || deviceId === (this._deviceId ?? ''))
3334
- && (!slotId || slotId === (this._slotId ?? ''));
3335
- }
3336
3720
  // ── Pull Gate(序列化同一 key 的并发 pull)──────────────────
3337
3721
  async _withBackgroundRpc(operation) {
3338
3722
  this._backgroundRpcDepth += 1;