@agentunion/fastaun-browser 0.5.6 → 0.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/_packed_docs/CHANGELOG.md +74 -0
  3. package/_packed_docs/INDEX.md +12 -11
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +4 -3
  5. package/_packed_docs/agent.md/SCHEMA.md +83 -58
  6. package/_packed_docs/agent.md/examples/codeagent-claudecode.md +1 -1
  7. package/_packed_docs/agent.md/examples/openclaw-lobster.md +1 -1
  8. package/_packed_docs/agent.md/examples/signed-openclaw-lobster.md +1 -1
  9. package/_packed_docs/audit/AUN/346/234/215/345/212/241Go/345/214/226/351/207/215/346/236/204/347/262/276/347/273/206/345/214/226/346/272/220/347/240/201/345/256/241/346/237/245-20260718.md +3 -3
  10. package/_packed_docs/aun/345/205/254/347/275/221/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +7 -0
  11. package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +422 -331
  12. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +598 -398
  13. package/_packed_docs/group-message-rpc-alignment-gaps.md +741 -0
  14. package/_packed_docs/message-online-push-alignment.md +572 -0
  15. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1 -1
  16. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +10 -10
  17. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +2 -2
  18. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +30 -10
  19. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +23 -20
  20. package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +3 -3
  21. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +5 -6
  22. package/_packed_docs/sdk/09-group-rpc-manual.md +11 -7
  23. package/_packed_docs/sdk/09-message-rpc-manual.md +5 -3
  24. package/_packed_docs/sdk/09-storage-rpc-manual.md +14 -3
  25. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +19 -17
  26. package/_packed_docs/sdk/CHANGELOG-0.5.6.md +73 -0
  27. package/_packed_docs/sdk/INDEX.md +22 -22
  28. package/_packed_docs/sdk/README.md +6 -5
  29. package/_packed_docs//345/217/221/345/270/203/346/212/245/345/221/212-0.5.6.md +260 -0
  30. package/dist/agent-md-schema.d.ts +14 -0
  31. package/dist/agent-md-schema.d.ts.map +1 -0
  32. package/dist/agent-md-schema.js +103 -0
  33. package/dist/agent-md-schema.js.map +1 -0
  34. package/dist/agent-md.d.ts +7 -0
  35. package/dist/agent-md.d.ts.map +1 -1
  36. package/dist/agent-md.js +229 -10
  37. package/dist/agent-md.js.map +1 -1
  38. package/dist/aid-store.d.ts +5 -1
  39. package/dist/aid-store.d.ts.map +1 -1
  40. package/dist/aid-store.js +26 -4
  41. package/dist/aid-store.js.map +1 -1
  42. package/dist/aid.d.ts +2 -0
  43. package/dist/aid.d.ts.map +1 -1
  44. package/dist/aid.js +29 -7
  45. package/dist/aid.js.map +1 -1
  46. package/dist/auth.d.ts.map +1 -1
  47. package/dist/auth.js +1 -0
  48. package/dist/auth.js.map +1 -1
  49. package/dist/bundle.js +29895 -21968
  50. package/dist/cert-utils.d.ts +1 -0
  51. package/dist/cert-utils.d.ts.map +1 -1
  52. package/dist/cert-utils.js +36 -16
  53. package/dist/cert-utils.js.map +1 -1
  54. package/dist/client/delivery.d.ts +33 -5
  55. package/dist/client/delivery.d.ts.map +1 -1
  56. package/dist/client/delivery.js +597 -220
  57. package/dist/client/delivery.js.map +1 -1
  58. package/dist/client/lifecycle.d.ts +15 -1
  59. package/dist/client/lifecycle.d.ts.map +1 -1
  60. package/dist/client/lifecycle.js +452 -151
  61. package/dist/client/lifecycle.js.map +1 -1
  62. package/dist/client/rpc-pipeline.d.ts +17 -6
  63. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  64. package/dist/client/rpc-pipeline.js +284 -115
  65. package/dist/client/rpc-pipeline.js.map +1 -1
  66. package/dist/client/runtime.d.ts +1 -3
  67. package/dist/client/runtime.d.ts.map +1 -1
  68. package/dist/client/runtime.js +4 -7
  69. package/dist/client/runtime.js.map +1 -1
  70. package/dist/client/v2-e2ee.d.ts +20 -2
  71. package/dist/client/v2-e2ee.d.ts.map +1 -1
  72. package/dist/client/v2-e2ee.js +398 -114
  73. package/dist/client/v2-e2ee.js.map +1 -1
  74. package/dist/client.d.ts +22 -10
  75. package/dist/client.d.ts.map +1 -1
  76. package/dist/client.js +396 -104
  77. package/dist/client.js.map +1 -1
  78. package/dist/facades.d.ts +23 -1
  79. package/dist/facades.d.ts.map +1 -1
  80. package/dist/facades.js +51 -7
  81. package/dist/facades.js.map +1 -1
  82. package/dist/group-id.d.ts.map +1 -1
  83. package/dist/group-id.js +2 -17
  84. package/dist/group-id.js.map +1 -1
  85. package/dist/index.d.ts +4 -3
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +2 -1
  88. package/dist/index.js.map +1 -1
  89. package/dist/seq-tracker.d.ts.map +1 -1
  90. package/dist/seq-tracker.js +8 -2
  91. package/dist/seq-tracker.js.map +1 -1
  92. package/dist/tools/cross-sdk-agent.js +23 -1
  93. package/dist/tools/cross-sdk-agent.js.map +1 -1
  94. package/dist/transport.d.ts.map +1 -1
  95. package/dist/transport.js +8 -1
  96. package/dist/transport.js.map +1 -1
  97. package/dist/validators.d.ts.map +1 -1
  98. package/dist/validators.js +8 -14
  99. package/dist/validators.js.map +1 -1
  100. package/dist/version.d.ts +1 -1
  101. package/dist/version.js +1 -1
  102. package/package.json +6 -5
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?.();
@@ -1173,14 +1189,102 @@ export class AUNClient {
1173
1189
  return;
1174
1190
  await this._handleTransportDisconnect(err);
1175
1191
  }
1176
- async createGroup(params = {}) {
1192
+ async _recoverIdentityAdmission(err, _method = '', params = {}) {
1193
+ if (!(err instanceof AUNError) || err.code !== -32003)
1194
+ return false;
1195
+ const message = String(err.message ?? '').trim();
1196
+ const groupCertPrefix = 'valid group identity certificate is required for ';
1197
+ if (message.toLowerCase().startsWith(groupCertPrefix)) {
1198
+ const fallback = message.slice(groupCertPrefix.length).trim();
1199
+ const store = this._aidStore;
1200
+ const groupId = String(params.group_id ?? params.groupId ?? params.group_aid ?? params.groupAid ?? fallback).trim();
1201
+ if (!groupId || !store)
1202
+ return false;
1203
+ try {
1204
+ await this._runGroupIdentityOperation(groupId, () => this.bindGroupAid({ group_id: groupId }, { aidStore: store }));
1205
+ }
1206
+ catch {
1207
+ return false;
1208
+ }
1209
+ return true;
1210
+ }
1211
+ const groupMDPrefix = 'valid group agent.md is required for ';
1212
+ if (message.toLowerCase().startsWith(groupMDPrefix)) {
1213
+ const groupAid = message.slice(groupMDPrefix.length).trim();
1214
+ const store = this._aidStore;
1215
+ if (!groupAid || !store)
1216
+ return false;
1217
+ const loaded = await store.load(groupAid);
1218
+ if (!loaded.ok || !loaded.data?.aid?.isPrivateKeyValid())
1219
+ return false;
1220
+ const ownerAid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
1221
+ if (!ownerAid)
1222
+ return false;
1223
+ await this._runGroupIdentityOperation(groupAid, () => this._uploadGroupAgentMd(store, groupAid, {}, {}, ownerAid));
1224
+ return true;
1225
+ }
1226
+ const agentPrefix = 'valid agent.md is required for ';
1227
+ if (!message.toLowerCase().startsWith(agentPrefix))
1228
+ return false;
1229
+ const target = message.slice(agentPrefix.length).trim();
1230
+ if (!target || target.toLowerCase() !== String(this._aid ?? '').trim().toLowerCase())
1231
+ return false;
1232
+ await this._agentMdManager.upload(buildDefaultAgentMd(target));
1233
+ return true;
1234
+ }
1235
+ async _runGroupIdentityOperation(groupId, operation) {
1236
+ const aid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
1237
+ const dot = aid.indexOf('.');
1238
+ const key = normalizeGroupId(groupId, { localIssuer: dot >= 0 ? aid.slice(dot + 1) : '' }).toLowerCase();
1239
+ if (!key)
1240
+ return operation();
1241
+ let task = this._groupIdentityOperations.get(key);
1242
+ if (!task) {
1243
+ task = Promise.resolve().then(operation);
1244
+ this._groupIdentityOperations.set(key, task);
1245
+ }
1246
+ try {
1247
+ return await task;
1248
+ }
1249
+ finally {
1250
+ if (this._groupIdentityOperations.get(key) === task)
1251
+ this._groupIdentityOperations.delete(key);
1252
+ }
1253
+ }
1254
+ async createGroup(params = {}, options = {}) {
1177
1255
  if (!isJsonObject(params)) {
1178
1256
  throw new ValidationError('createGroup params must be an object');
1179
1257
  }
1258
+ const store = options.aidStore ?? this._aidStore;
1259
+ if (!store) {
1260
+ throw new ValidationError('createGroup requires aidStore');
1261
+ }
1262
+ this._aidStore = store;
1180
1263
  const payload = { ...params };
1181
- const groupName = String(payload.group_name ?? payload.groupName ?? '').trim();
1182
- if (!groupName) {
1183
- return await this.call('group.create', payload);
1264
+ const isNamed = Boolean(String(payload.group_name ?? '').trim());
1265
+ payload._defer_group_ready_postprocess = true;
1266
+ if (!isNamed) {
1267
+ delete payload.public_key;
1268
+ delete payload.curve;
1269
+ const created = await this.call('group.create', payload);
1270
+ const createdMap = isJsonObject(created) ? created : {};
1271
+ const createdGroup = isJsonObject(createdMap.group) ? createdMap.group : {};
1272
+ const groupId = String(createdGroup.group_id ?? createdGroup.group_aid ?? createdMap.group_id ?? createdMap.group_aid ?? '').trim();
1273
+ if (!groupId) {
1274
+ throw new ValidationError('group.create response missing group.group_id');
1275
+ }
1276
+ const bindParams = { group_id: groupId };
1277
+ for (const key of ['name', 'description', 'group_description', 'group_agent_md', 'groupAgentMd', 'content']) {
1278
+ if (key in payload)
1279
+ bindParams[key] = payload[key];
1280
+ }
1281
+ const bound = await this._runGroupIdentityOperation(groupId, () => this.bindGroupAid(bindParams, { aidStore: store }));
1282
+ const boundMap = isJsonObject(bound) ? bound : {};
1283
+ const boundGroup = isJsonObject(boundMap.group) ? boundMap.group : {};
1284
+ const result = { ...createdMap, ...boundMap, group: { ...createdGroup, ...boundGroup } };
1285
+ const postprocessParams = { ...payload };
1286
+ delete postprocessParams._defer_group_ready_postprocess;
1287
+ return await this._groupState.postprocessResult('group.create', postprocessParams, result);
1184
1288
  }
1185
1289
  const keyPair = await new CryptoProvider().generateIdentity();
1186
1290
  payload.public_key = keyPair.public_key_der_b64;
@@ -1192,30 +1296,63 @@ export class AUNClient {
1192
1296
  const groupAid = String(group.group_aid ?? resultMap.group_aid ?? '').trim();
1193
1297
  const certPem = String(aidCert.cert ?? aidCert.cert_pem ?? resultMap.cert ?? resultMap.cert_pem ?? '').trim();
1194
1298
  if (!groupAid || !certPem) {
1195
- throw new ValidationError('group.create named group response missing group.group_aid or aid_cert.cert');
1299
+ throw new ValidationError('group.create response missing group.group_aid or aid_cert.cert');
1196
1300
  }
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,
1301
+ const imported = await store.importGroupIdentity(groupAid, {
1302
+ private_key_pem: keyPair.private_key_pem,
1303
+ public_key_der_b64: keyPair.public_key_der_b64,
1304
+ curve: keyPair.curve,
1305
+ cert_pem: certPem,
1203
1306
  });
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
- }
1307
+ if (!imported.ok) {
1308
+ throw new ValidationError(imported.error.message);
1214
1309
  }
1215
- finally {
1216
- store.close();
1310
+ const uploaderAid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
1311
+ if (!uploaderAid) {
1312
+ throw new ValidationError('createGroup requires current owner AID for group agent.md upload');
1313
+ }
1314
+ await this._uploadGroupAgentMd(store, groupAid, payload, group, uploaderAid);
1315
+ const postprocessParams = { ...payload };
1316
+ delete postprocessParams._defer_group_ready_postprocess;
1317
+ return await this._groupState.postprocessResult('group.create', postprocessParams, result);
1318
+ }
1319
+ _groupAgentMdContent(groupAid, params = {}, group = {}) {
1320
+ const explicit = params.group_agent_md ?? params.groupAgentMd ?? params.content;
1321
+ if (explicit !== undefined && explicit !== null)
1322
+ return String(explicit);
1323
+ const value = (...keys) => {
1324
+ for (const source of [group, params]) {
1325
+ for (const key of keys) {
1326
+ const candidate = String(source[key] ?? '').trim();
1327
+ if (candidate)
1328
+ return candidate;
1329
+ }
1330
+ }
1331
+ return '';
1332
+ };
1333
+ const name = value('name', 'group_name', 'title') || groupAid;
1334
+ const normalizedDescription = (value('description', 'group_description') || `AUN group ${name}`)
1335
+ .split(/\s+/).filter(Boolean).join(' ');
1336
+ const description = Array.from(normalizedDescription).slice(0, 100).join('');
1337
+ return [
1338
+ '---\n',
1339
+ `aid: ${JSON.stringify(groupAid)}\n`,
1340
+ `name: ${JSON.stringify(name)}\n`,
1341
+ 'type: "group"\n',
1342
+ 'version: "1.0.0"\n',
1343
+ `description: ${JSON.stringify(description)}\n`,
1344
+ '---\n\n',
1345
+ `# ${name}\n`,
1346
+ ].join('');
1347
+ }
1348
+ async _uploadGroupAgentMd(store, groupAid, params, group, uploaderAid) {
1349
+ if (typeof store.uploadAgentMd !== 'function') {
1350
+ throw new ValidationError('group agent.md upload is unavailable');
1351
+ }
1352
+ const uploaded = await store.uploadAgentMd(groupAid, this._groupAgentMdContent(groupAid, params, group), { uploaderAid });
1353
+ if (!uploaded.ok) {
1354
+ throw new ValidationError(`group agent.md upload failed: ${uploaded.error.message}`);
1217
1355
  }
1218
- return result;
1219
1356
  }
1220
1357
  async bindGroupAid(params = {}, options = {}) {
1221
1358
  if (!isJsonObject(params)) {
@@ -1225,6 +1362,7 @@ export class AUNClient {
1225
1362
  if (!store) {
1226
1363
  throw new ValidationError('bindGroupAid requires aidStore');
1227
1364
  }
1365
+ this._aidStore = store;
1228
1366
  const groupId = String(params.group_id ?? params.group_aid ?? '').trim();
1229
1367
  const keystore = store._keystore;
1230
1368
  let keyPair = null;
@@ -1272,6 +1410,11 @@ export class AUNClient {
1272
1410
  if (!imported.ok) {
1273
1411
  throw new ValidationError(imported.error.message);
1274
1412
  }
1413
+ const uploaderAid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
1414
+ if (!uploaderAid) {
1415
+ throw new ValidationError('bindGroupAid requires current owner AID for group agent.md upload');
1416
+ }
1417
+ await this._uploadGroupAgentMd(store, groupAid, payload, group, uploaderAid);
1275
1418
  // 落盘成功,清除 pending 槽位
1276
1419
  if (groupId && keystore && typeof keystore.clearPendingGroupBind === 'function') {
1277
1420
  await keystore.clearPendingGroupBind(groupId);
@@ -1293,7 +1436,7 @@ export class AUNClient {
1293
1436
  let groupAid = String(params.group_aid ?? '').trim();
1294
1437
  if (!groupAid) {
1295
1438
  const info = await this.call('group.get_info', { group_id: groupId, required: ['member'] });
1296
- groupAid = String(info?.group_aid ?? '').trim();
1439
+ groupAid = String(info?.group_aid ?? info?.group?.group_aid ?? '').trim();
1297
1440
  }
1298
1441
  if (!groupAid) {
1299
1442
  throw new ValidationError('renewGroupAid: unable to determine group_aid');
@@ -1361,6 +1504,11 @@ export class AUNClient {
1361
1504
  if (!imported.ok) {
1362
1505
  throw new ValidationError(`renewGroupAid failed to persist group identity: ${imported.error.message}`);
1363
1506
  }
1507
+ const uploaderAid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
1508
+ if (!uploaderAid) {
1509
+ throw new ValidationError('renewGroupAid requires current owner AID for group agent.md upload');
1510
+ }
1511
+ await this._uploadGroupAgentMd(store, returnedGroupAid, payload, group, uploaderAid);
1364
1512
  return result;
1365
1513
  }
1366
1514
  async startGroupTransfer(params = {}, options = {}) {
@@ -1379,7 +1527,7 @@ export class AUNClient {
1379
1527
  let groupAid = String(params.group_aid ?? '').trim();
1380
1528
  if (!groupAid) {
1381
1529
  const info = await this.call('group.get_info', { group_id: groupId, required: ['member'] });
1382
- groupAid = String(info?.group_aid ?? '').trim();
1530
+ groupAid = String(info?.group_aid ?? info?.group?.group_aid ?? '').trim();
1383
1531
  }
1384
1532
  if (!groupAid) {
1385
1533
  throw new ValidationError('startGroupTransfer: unable to determine group_aid');
@@ -1427,7 +1575,7 @@ export class AUNClient {
1427
1575
  let groupAid = String(params.group_aid ?? '').trim();
1428
1576
  if (!groupAid) {
1429
1577
  const info = await this.call('group.get_info', { group_id: groupId, required: ['member'] });
1430
- groupAid = String(info?.group_aid ?? '').trim();
1578
+ groupAid = String(info?.group_aid ?? info?.group?.group_aid ?? '').trim();
1431
1579
  }
1432
1580
  if (!groupAid) {
1433
1581
  throw new ValidationError('completeGroupTransfer: unable to determine group_aid');
@@ -1475,6 +1623,11 @@ export class AUNClient {
1475
1623
  if (!imported.ok) {
1476
1624
  throw new ValidationError(imported.error.message);
1477
1625
  }
1626
+ const uploaderAid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
1627
+ if (!uploaderAid) {
1628
+ throw new ValidationError('completeGroupTransfer requires current owner AID for group agent.md upload');
1629
+ }
1630
+ await this._uploadGroupAgentMd(store, returnedGroupAid, payload, group, uploaderAid);
1478
1631
  return result;
1479
1632
  }
1480
1633
  static _notifyParamsSizeOk(params) {
@@ -1692,14 +1845,14 @@ export class AUNClient {
1692
1845
  const trace = `${this._echoTimestamp()} [AUN-SDK.receive] aid=${this._aid ?? '-'} conn_uptime=${uptime}s`;
1693
1846
  msg.payload = { ...payload, text: payload.text + '\n' + trace };
1694
1847
  }
1695
- async _drainOrderedMessages(ns, beforeSeq) {
1696
- await this._delivery.drainOrderedMessages(ns, beforeSeq);
1848
+ async _drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true) {
1849
+ await this._delivery.drainOrderedMessages(ns, beforeSeq, pullResponse, persist);
1697
1850
  }
1698
1851
  async _publishOrderedMessage(event, ns, seq, payload) {
1699
1852
  return this._delivery.publishOrderedMessage(event, ns, seq, payload);
1700
1853
  }
1701
- async _publishPulledMessage(event, ns, seq, payload) {
1702
- return this._delivery.publishPulledMessage(event, ns, seq, payload);
1854
+ async _publishPulledMessage(event, ns, seq, payload, persist = true) {
1855
+ return this._delivery.publishPulledMessage(event, ns, seq, payload, persist);
1703
1856
  }
1704
1857
  _extractGroupIdFromResult(result) {
1705
1858
  const group = isJsonObject(result.group) ? result.group : null;
@@ -1937,8 +2090,10 @@ export class AUNClient {
1937
2090
  await this._rpcPipeline.signClientOperation(method, params);
1938
2091
  }
1939
2092
  // ── 内部:连接 ────────────────────────────────────
1940
- async _connectOnce(params, allowReauth) {
2093
+ async _connectOnce(params, allowReauth, reconnectOwner, connectionOwner) {
1941
2094
  const tStart = Date.now();
2095
+ connectionOwner ??= this._connectAbort ?? undefined;
2096
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
1942
2097
  this._clientLog.debug(`_connectOnce enter: allow_reauth=${allowReauth}`);
1943
2098
  try {
1944
2099
  const gatewayUrl = this._resolveGateway(params);
@@ -1951,8 +2106,12 @@ export class AUNClient {
1951
2106
  // 避免 reader 把积压 push 交给空 tracker 的 handler,触发 S2 历史 gap 误补拉。
1952
2107
  this._refreshSeqTrackerContext();
1953
2108
  await this._restoreSeqTrackerState();
2109
+ this._assertReconnectOwner(reconnectOwner);
2110
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
1954
2111
  try {
1955
2112
  const challenge = await this._transport.connect(gatewayUrl);
2113
+ this._assertReconnectOwner(reconnectOwner);
2114
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
1956
2115
  this._state = 'authenticating';
1957
2116
  if (allowReauth) {
1958
2117
  const authContext = await this._auth.connectSession(this._transport, challenge, gatewayUrl, {
@@ -1964,6 +2123,8 @@ export class AUNClient {
1964
2123
  shortTtlMs: Number(params.short_ttl_ms ?? 0),
1965
2124
  extraInfo: params.extra_info,
1966
2125
  });
2126
+ this._assertReconnectOwner(reconnectOwner);
2127
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
1967
2128
  if (isJsonObject(authContext)) {
1968
2129
  const auth = authContext;
1969
2130
  const identity = auth.identity;
@@ -1988,7 +2149,10 @@ export class AUNClient {
1988
2149
  shortTtlMs: Number(params.short_ttl_ms ?? 0),
1989
2150
  extraInfo: params.extra_info,
1990
2151
  });
2152
+ this._assertReconnectOwner(reconnectOwner);
2153
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
1991
2154
  await this._syncIdentityAfterConnect(String(params.access_token));
2155
+ this._assertReconnectOwner(reconnectOwner);
1992
2156
  if (isJsonObject(hello) && 'heartbeat_interval' in hello) {
1993
2157
  this._applyServerHeartbeatInterval(hello.heartbeat_interval, 'auth');
1994
2158
  }
@@ -1996,13 +2160,19 @@ export class AUNClient {
1996
2160
  }
1997
2161
  catch (err) {
1998
2162
  // P1-19: 首连失败时重置状态,避免半连接残留
1999
- this._state = 'standby';
2163
+ if ((!reconnectOwner || this._ownsReconnect(reconnectOwner))
2164
+ && (!connectionOwner || this._lifecycle.ownsConnectionAttempt(connectionOwner))
2165
+ && !this._closing) {
2166
+ this._state = 'standby';
2167
+ }
2000
2168
  try {
2001
2169
  await this._transport.close();
2002
2170
  }
2003
2171
  catch { /* 忽略关闭错误 */ }
2004
2172
  throw err;
2005
2173
  }
2174
+ this._assertReconnectOwner(reconnectOwner);
2175
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
2006
2176
  if (!this._transport.isConnected()) {
2007
2177
  throw new ConnectionError('websocket disconnected during authentication');
2008
2178
  }
@@ -2017,14 +2187,30 @@ export class AUNClient {
2017
2187
  else if (gatewayUrl) {
2018
2188
  this._gatewayCandidates = [gatewayUrl];
2019
2189
  }
2020
- await this._dispatcher.publish('state_change', {
2190
+ const statePayload = {
2021
2191
  state: this._publicState(this._state),
2022
2192
  gateway: gatewayUrl,
2023
- });
2193
+ };
2194
+ if (reconnectOwner) {
2195
+ if (!await this._publishReconnectEvent(reconnectOwner, 'state_change', statePayload)) {
2196
+ throw new ConnectionError('reconnect attempt superseded');
2197
+ }
2198
+ }
2199
+ else if (connectionOwner) {
2200
+ if (!await this._lifecycle.publishConnectionEvent(connectionOwner, 'state_change', statePayload)) {
2201
+ throw new StateError('connection attempt superseded');
2202
+ }
2203
+ }
2204
+ else {
2205
+ await this._dispatcher.publish('state_change', statePayload);
2206
+ }
2207
+ this._assertReconnectOwner(reconnectOwner);
2208
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
2024
2209
  if (this._seqTrackerContext !== this._currentSeqTrackerContext()) {
2025
2210
  this._refreshSeqTrackerContext();
2026
2211
  await this._restoreSeqTrackerState();
2027
2212
  }
2213
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
2028
2214
  this._startBackgroundTasks();
2029
2215
  const connectionKind = String(params.connection_kind ?? 'long');
2030
2216
  const isShortConnection = connectionKind === 'short';
@@ -2033,6 +2219,8 @@ export class AUNClient {
2033
2219
  && (!isShortConnection || hasExplicitBackgroundSync);
2034
2220
  if (!isShortConnection) {
2035
2221
  await this._v2E2EE.onConnected({ backgroundSync: backgroundSyncEnabled });
2222
+ this._assertReconnectOwner(reconnectOwner);
2223
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
2036
2224
  }
2037
2225
  else {
2038
2226
  this._clientLog.debug('V2 session init deferred for short connection');
@@ -2442,13 +2630,16 @@ export class AUNClient {
2442
2630
  this._clientLog.debug(`publish gateway.disconnect failed: ${exc?.message ?? exc}`);
2443
2631
  }
2444
2632
  }
2445
- async _handleTransportDisconnect(error, closeCode) {
2633
+ async _handleTransportDisconnect(error, closeCode, initialOptions) {
2446
2634
  if (this._closing || this._state === 'closed')
2447
2635
  return;
2448
2636
  // 在首个 await 前占位,避免 close/error 与 token 刷新并发启动多条重连链。
2449
2637
  if (this._reconnectActive)
2450
2638
  return;
2451
2639
  this._reconnectActive = true;
2640
+ // 在首个 await 前创建取消控制器,覆盖 disconnect 与清理动作的竞态窗口。
2641
+ const reconnectAbort = new AbortController();
2642
+ this._reconnectAbort = reconnectAbort;
2452
2643
  // 不等待状态事件或 transport 清理,先隔离已在途的 inline Push 回调。
2453
2644
  this._delivery.resetInlineAckState();
2454
2645
  this._state = 'standby';
@@ -2464,8 +2655,18 @@ export class AUNClient {
2464
2655
  state: this._publicState(this._state),
2465
2656
  error,
2466
2657
  });
2658
+ if (reconnectAbort.signal.aborted || this._reconnectAbort !== reconnectAbort || this._closing) {
2659
+ if (this._reconnectAbort === reconnectAbort) {
2660
+ this._reconnectAbort = null;
2661
+ this._reconnectActive = false;
2662
+ }
2663
+ return;
2664
+ }
2467
2665
  if (!this._sessionOptions.auto_reconnect) {
2468
- this._reconnectActive = false;
2666
+ if (this._reconnectAbort === reconnectAbort) {
2667
+ this._reconnectAbort = null;
2668
+ this._reconnectActive = false;
2669
+ }
2469
2670
  return;
2470
2671
  }
2471
2672
  // 不重连 close code(认证失败/权限错误/被踢等)或服务端通知断开:抑制重连
@@ -2487,30 +2688,90 @@ export class AUNClient {
2487
2688
  eventPayload.code = disconnectInfo.code;
2488
2689
  }
2489
2690
  await this._dispatcher.publish('state_change', eventPayload);
2490
- this._reconnectActive = false;
2691
+ if (this._reconnectAbort === reconnectAbort) {
2692
+ this._reconnectAbort = null;
2693
+ this._reconnectActive = false;
2694
+ }
2491
2695
  return;
2492
2696
  }
2493
2697
  // 1000 = 正常关闭, 1006 = 网络异常断开(无 close frame),其他 code = 服务端主动关闭
2494
2698
  const serverInitiated = closeCode !== undefined && closeCode !== 1000 && closeCode !== 1006;
2495
- if (this._closing) {
2496
- this._reconnectActive = false;
2699
+ if (this._closing || reconnectAbort.signal.aborted || this._reconnectAbort !== reconnectAbort) {
2700
+ if (this._reconnectAbort === reconnectAbort) {
2701
+ this._reconnectAbort = null;
2702
+ this._reconnectActive = false;
2703
+ }
2497
2704
  return;
2498
2705
  }
2499
- const reconnectAbort = new AbortController();
2500
- this._reconnectAbort = reconnectAbort;
2501
- this._safeAsync(this._reconnectLoop(serverInitiated, reconnectAbort));
2706
+ const reconnectTask = Promise.resolve().then(() => this._reconnectLoop(serverInitiated, reconnectAbort, initialOptions));
2707
+ this._reconnectTask = reconnectTask;
2708
+ this._safeAsync(reconnectTask);
2709
+ }
2710
+ _ownsReconnect(owner) {
2711
+ return Boolean(owner
2712
+ && !owner.signal.aborted
2713
+ && !this._closing
2714
+ && this._reconnectAbort === owner);
2715
+ }
2716
+ _assertReconnectOwner(owner) {
2717
+ if (owner && !this._ownsReconnect(owner)) {
2718
+ throw new ConnectionError('reconnect attempt superseded');
2719
+ }
2720
+ }
2721
+ async _publishReconnectEvent(owner, event, payload) {
2722
+ if (!this._ownsReconnect(owner))
2723
+ return false;
2724
+ this._reconnectEventDispatchDepth += 1;
2725
+ try {
2726
+ await this._dispatcher.publish(event, payload);
2727
+ }
2728
+ finally {
2729
+ this._reconnectEventDispatchDepth -= 1;
2730
+ }
2731
+ return this._ownsReconnect(owner);
2732
+ }
2733
+ async _cancelReconnectAndWait() {
2734
+ const owner = this._reconnectAbort;
2735
+ const task = this._reconnectTask;
2736
+ if (!owner && !task && !this._reconnectActive)
2737
+ return;
2738
+ owner?.abort();
2739
+ try {
2740
+ await this._transport.close();
2741
+ }
2742
+ catch (exc) {
2743
+ this._clientLog.debug(`reconnect cancellation transport cleanup skipped: ${formatCaughtError(exc)}`);
2744
+ }
2745
+ // state_change handler 内等待其所属重连任务会自锁;该路径由事件返回后的
2746
+ // owner 复核负责收尾,且事件回调期间尚未进入下一次 transport 操作。
2747
+ if (task && this._reconnectEventDispatchDepth === 0) {
2748
+ try {
2749
+ await task;
2750
+ }
2751
+ catch (exc) {
2752
+ this._clientLog.debug(`cancelled reconnect task exited with error: ${formatCaughtError(exc)}`);
2753
+ }
2754
+ }
2755
+ if (this._reconnectAbort === owner) {
2756
+ this._reconnectAbort = null;
2757
+ this._reconnectActive = false;
2758
+ this._nextRetryAt = null;
2759
+ }
2760
+ if (this._reconnectTask === task)
2761
+ this._reconnectTask = null;
2502
2762
  }
2503
2763
  /** 重连循环入口;无论正常退出还是意外异常,都只清理自己这一代控制器。 */
2504
- async _reconnectLoop(serverInitiated = false, reconnectAbort = this._reconnectAbort) {
2764
+ async _reconnectLoop(serverInitiated = false, reconnectAbort = this._reconnectAbort, initialOptions) {
2505
2765
  let releasedOwnGeneration = false;
2506
2766
  try {
2507
- await this._runReconnectLoop(serverInitiated, reconnectAbort);
2767
+ await this._runReconnectLoop(serverInitiated, reconnectAbort, initialOptions);
2508
2768
  }
2509
2769
  finally {
2510
2770
  if (this._reconnectAbort === reconnectAbort) {
2511
2771
  this._nextRetryAt = null;
2512
2772
  this._reconnectActive = false;
2513
2773
  this._reconnectAbort = null;
2774
+ this._reconnectTask = null;
2514
2775
  releasedOwnGeneration = true;
2515
2776
  }
2516
2777
  }
@@ -2519,7 +2780,7 @@ export class AUNClient {
2519
2780
  }
2520
2781
  }
2521
2782
  /** 指数退避 + 当前基数抖动重连循环主体。 */
2522
- async _runReconnectLoop(serverInitiated, reconnectAbort) {
2783
+ async _runReconnectLoop(serverInitiated, reconnectAbort, initialOptions) {
2523
2784
  const retry = { ...this._sessionOptions.retry };
2524
2785
  const maxBaseDelay = clampReconnectDelaySeconds(retry.max_delay, RECONNECT_MAX_BASE_DELAY_SECONDS);
2525
2786
  // M25: max_attempts=0 表示无限重试(与 Go/Python 对齐)
@@ -2534,11 +2795,12 @@ export class AUNClient {
2534
2795
  if (maxAttempts > 0 && attempt > maxAttempts) {
2535
2796
  this._state = 'terminal_failed';
2536
2797
  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
- });
2798
+ if (reconnectAbort)
2799
+ await this._publishReconnectEvent(reconnectAbort, 'state_change', {
2800
+ state: this._publicState(this._state),
2801
+ attempt: attempt - 1,
2802
+ reason: 'max_attempts_exhausted',
2803
+ });
2542
2804
  return;
2543
2805
  }
2544
2806
  // 先进入 retry_backoff 状态(对齐 Python:先退避再重连)
@@ -2546,22 +2808,26 @@ export class AUNClient {
2546
2808
  const sleepMs = reconnectSleepDelaySeconds(delay, maxBaseDelay) * 1000;
2547
2809
  this._nextRetryAt = new Date(Date.now() + sleepMs);
2548
2810
  this._state = 'retry_backoff';
2549
- await this._dispatcher.publish('state_change', {
2811
+ if (!reconnectAbort || !await this._publishReconnectEvent(reconnectAbort, 'state_change', {
2550
2812
  state: this._publicState(this._state),
2551
2813
  attempt,
2552
2814
  next_retry_at: this._nextRetryAt.getTime() / 1000,
2553
- });
2815
+ }))
2816
+ return;
2554
2817
  try {
2555
2818
  await this._sleep(sleepMs, reconnectAbort?.signal);
2556
2819
  this._nextRetryAt = null;
2557
- if (reconnectAbort?.signal.aborted)
2820
+ if (!this._ownsReconnect(reconnectAbort))
2558
2821
  return;
2559
2822
  // 退避结束,进入 reconnecting 状态
2560
2823
  this._state = 'reconnecting';
2561
- await this._dispatcher.publish('state_change', {
2824
+ if (!reconnectAbort || !await this._publishReconnectEvent(reconnectAbort, 'state_change', {
2562
2825
  state: this._publicState(this._state),
2563
2826
  attempt,
2564
- });
2827
+ }))
2828
+ return;
2829
+ if (!this._ownsReconnect(reconnectAbort))
2830
+ return;
2565
2831
  // /health 仅作旁路诊断,不能阻止实际 WebSocket 重连。
2566
2832
  if (this._gatewayUrl) {
2567
2833
  void this._discovery.checkHealth(this._gatewayUrl, 5000)
@@ -2574,6 +2840,20 @@ export class AUNClient {
2574
2840
  });
2575
2841
  }
2576
2842
  await this._transport.close();
2843
+ if (!this._ownsReconnect(reconnectAbort))
2844
+ return;
2845
+ if (initialOptions) {
2846
+ // 首连失败时没有可复用的 session/gateway,必须重新 discovery + authenticate。
2847
+ await this._lifecycle.retryInitialConnect(initialOptions, reconnectAbort ?? undefined);
2848
+ if (!this._ownsReconnect(reconnectAbort))
2849
+ return;
2850
+ if (!this._transport.isConnected()) {
2851
+ throw new ConnectionError('websocket disconnected while initial reconnect completed');
2852
+ }
2853
+ this._clientLog.debug(`initial reconnect success: attempt=${attempt}, aid=${this._aid ?? ''}`);
2854
+ this._nextRetryAt = null;
2855
+ return;
2856
+ }
2577
2857
  if (!this._sessionParams) {
2578
2858
  throw new StateError('missing connect params for reconnect');
2579
2859
  }
@@ -2601,7 +2881,9 @@ export class AUNClient {
2601
2881
  for (const gateway of candidates) {
2602
2882
  const attemptParams = { ...this._sessionParams, gateway };
2603
2883
  try {
2604
- await this._connectOnce(attemptParams, true);
2884
+ await this._connectOnce(attemptParams, true, reconnectAbort ?? undefined);
2885
+ if (!this._ownsReconnect(reconnectAbort))
2886
+ return;
2605
2887
  if (!this._transport.isConnected()) {
2606
2888
  throw new ConnectionError('websocket disconnected while reconnecting');
2607
2889
  }
@@ -2627,20 +2909,24 @@ export class AUNClient {
2627
2909
  return;
2628
2910
  }
2629
2911
  catch (exc) {
2912
+ if (!this._ownsReconnect(reconnectAbort))
2913
+ return;
2630
2914
  this._lastError = exc instanceof Error ? exc : new Error(String(exc));
2631
2915
  this._lastErrorCode = 'reconnect_failed';
2632
- await this._dispatcher.publish('connection.error', {
2916
+ if (!reconnectAbort || !await this._publishReconnectEvent(reconnectAbort, 'connection.error', {
2633
2917
  error: formatCaughtError(exc),
2634
2918
  attempt,
2635
- });
2919
+ }))
2920
+ return;
2636
2921
  if (!this._shouldRetryReconnect(exc)) {
2637
2922
  this._state = 'terminal_failed';
2638
2923
  this._nextRetryAt = null;
2639
- await this._dispatcher.publish('state_change', {
2640
- state: this._publicState(this._state),
2641
- error: formatCaughtError(exc),
2642
- attempt,
2643
- });
2924
+ if (reconnectAbort)
2925
+ await this._publishReconnectEvent(reconnectAbort, 'state_change', {
2926
+ state: this._publicState(this._state),
2927
+ error: formatCaughtError(exc),
2928
+ attempt,
2929
+ });
2644
2930
  return;
2645
2931
  }
2646
2932
  delay = Math.min(delay * 2, maxBaseDelay);
@@ -2663,7 +2949,7 @@ export class AUNClient {
2663
2949
  || error instanceof ValidationError || error instanceof StateError) {
2664
2950
  return false;
2665
2951
  }
2666
- if (error instanceof ConnectionError)
2952
+ if (error instanceof ConnectionError || error instanceof TimeoutError)
2667
2953
  return true;
2668
2954
  if (error instanceof AUNError)
2669
2955
  return error.retryable;
@@ -2683,6 +2969,7 @@ export class AUNClient {
2683
2969
  _resetSeqTrackingState() {
2684
2970
  void this._saveSeqTrackerState();
2685
2971
  this._resetV2IdentityRuntime();
2972
+ this._v2E2EE.resetForwardCursorState();
2686
2973
  this._seqTracker = new SeqTracker();
2687
2974
  this._seqTrackerContext = null;
2688
2975
  this._gapFillDone.clear();
@@ -2692,18 +2979,13 @@ export class AUNClient {
2692
2979
  this._v2SenderIKPending.clear();
2693
2980
  this._v2SenderIKFetching.clear();
2694
2981
  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
2982
  }
2702
2983
  _refreshSeqTrackerContext() {
2703
2984
  const nextContext = this._currentSeqTrackerContext();
2704
2985
  if (nextContext === this._seqTrackerContext)
2705
2986
  return;
2706
2987
  void this._saveSeqTrackerState();
2988
+ this._v2E2EE.resetForwardCursorState();
2707
2989
  this._seqTracker = new SeqTracker();
2708
2990
  this._delivery.resetInlineAckState();
2709
2991
  this._gapFillDone.clear();
@@ -2713,12 +2995,6 @@ export class AUNClient {
2713
2995
  this._v2SenderIKPending.clear();
2714
2996
  this._v2SenderIKFetching.clear();
2715
2997
  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
2998
  this._seqTrackerContext = nextContext;
2723
2999
  }
2724
3000
  /** 将 SeqTracker 状态保存到 keystore */
@@ -2941,7 +3217,7 @@ export class AUNClient {
2941
3217
  * undecryptable 事件、成功消费后触发 SPK 轮换。inline Push 在游标提交和
2942
3218
  * Head/正文绑定校验前调用本入口时必须显式关闭这些副作用。
2943
3219
  */
2944
- async _decryptV2Message(msg, allowPending = true, emitUndecryptable = true, rotateKeys = true, observeAgentMd = true) {
3220
+ async _decryptV2Message(msg, allowPending = true, emitUndecryptable = true, rotateKeys = true, observeAgentMd = true, deferStatus, deferKeyFetch = false) {
2945
3221
  const session = this._v2Session;
2946
3222
  if (!session)
2947
3223
  return null;
@@ -3008,6 +3284,24 @@ export class AUNClient {
3008
3284
  }
3009
3285
  catch (exc) {
3010
3286
  this._clientLog.warn(`V2 decrypt: SPK lookup failed seq=${String(msg.seq)} spk_id=${spkId}: ${String(exc)}`);
3287
+ if (allowPending || deferStatus) {
3288
+ const fromAid = String(msg.from_aid ?? '');
3289
+ const senderDeviceId = String(aad.from_device ?? '');
3290
+ if (deferStatus) {
3291
+ deferStatus.deferred = true;
3292
+ deferStatus.fromAid = fromAid;
3293
+ deferStatus.senderDeviceId = senderDeviceId;
3294
+ deferStatus.groupId = groupIdForKeys;
3295
+ }
3296
+ this._scheduleV2SenderIKPending({
3297
+ msg,
3298
+ fromAid,
3299
+ senderDeviceId,
3300
+ groupId: groupIdForKeys,
3301
+ startFetch: !deferKeyFetch,
3302
+ });
3303
+ return null;
3304
+ }
3011
3305
  if (emitUndecryptable) {
3012
3306
  try {
3013
3307
  const event = {
@@ -3035,13 +3329,19 @@ export class AUNClient {
3035
3329
  const senderDeviceId = String(aad.from_device ?? '');
3036
3330
  const senderCertFingerprint = String(envelope.sender_cert_fingerprint ?? '').trim().toLowerCase();
3037
3331
  let senderPubDer = await this._getV2SenderPubDer(fromAid, senderDeviceId, senderCertFingerprint);
3038
- if (!senderPubDer) {
3332
+ if (!senderPubDer && !deferKeyFetch) {
3039
3333
  this._clientLog.warn(`V2 decrypt: no sender IK for ${fromAid} device=${senderDeviceId}`);
3040
3334
  senderPubDer = await this._fetchV2SenderIKOnce(fromAid, senderDeviceId, groupIdForKeys, senderCertFingerprint);
3041
3335
  }
3042
3336
  if (!senderPubDer) {
3043
- if (allowPending) {
3044
- this._scheduleV2SenderIKPending({ msg, fromAid, senderDeviceId, groupId: groupIdForKeys });
3337
+ if (allowPending || deferStatus) {
3338
+ if (deferStatus) {
3339
+ deferStatus.deferred = true;
3340
+ deferStatus.fromAid = fromAid;
3341
+ deferStatus.senderDeviceId = senderDeviceId;
3342
+ deferStatus.groupId = groupIdForKeys;
3343
+ }
3344
+ this._scheduleV2SenderIKPending({ msg, fromAid, senderDeviceId, groupId: groupIdForKeys, startFetch: !deferKeyFetch });
3045
3345
  return null;
3046
3346
  }
3047
3347
  if (emitUndecryptable) {
@@ -3069,9 +3369,13 @@ export class AUNClient {
3069
3369
  let plaintext;
3070
3370
  try {
3071
3371
  plaintext = await decryptMessage(envelope, this._aid ?? '', this._deviceId, ikPriv, spkPriv, senderPubDer);
3372
+ if (plaintext === null)
3373
+ throw new Error('recipient_not_found_or_envelope_verification_failed');
3072
3374
  }
3073
3375
  catch (exc) {
3074
3376
  this._clientLog.warn(`V2 decrypt failed for msg seq=${String(msg.seq)}: ${String(exc)}`);
3377
+ if (deferStatus)
3378
+ deferStatus.terminal = true;
3075
3379
  if (emitUndecryptable) {
3076
3380
  try {
3077
3381
  const event = {
@@ -3321,18 +3625,6 @@ export class AUNClient {
3321
3625
  this._clientLog.warn(`background task exception:${String(exc)}`);
3322
3626
  });
3323
3627
  }
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
3628
  // ── Pull Gate(序列化同一 key 的并发 pull)──────────────────
3337
3629
  async _withBackgroundRpc(operation) {
3338
3630
  this._backgroundRpcDepth += 1;