@agentunion/fastaun 0.5.35 → 0.5.37

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 (49) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/_packed_docs/CHANGELOG.md +40 -0
  3. package/_packed_docs/INDEX.md +10 -3
  4. package/_packed_docs/sdk/0.5.36-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +130 -0
  5. package/_packed_docs/sdk/0.5.36-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +176 -0
  6. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +6 -6
  7. package/_packed_docs/sdk/INDEX.md +15 -10
  8. package/dist/agent-md.d.ts +2 -1
  9. package/dist/agent-md.js +34 -6
  10. package/dist/agent-md.js.map +1 -1
  11. package/dist/aid-store.js +5 -1
  12. package/dist/aid-store.js.map +1 -1
  13. package/dist/auth.js +5 -2
  14. package/dist/auth.js.map +1 -1
  15. package/dist/client/control-messages.js +4 -2
  16. package/dist/client/control-messages.js.map +1 -1
  17. package/dist/client/delivery.d.ts +2 -0
  18. package/dist/client/delivery.js +111 -40
  19. package/dist/client/delivery.js.map +1 -1
  20. package/dist/client/lifecycle.js +1 -1
  21. package/dist/client/lifecycle.js.map +1 -1
  22. package/dist/client/rpc-pipeline.d.ts +3 -0
  23. package/dist/client/rpc-pipeline.js +45 -3
  24. package/dist/client/rpc-pipeline.js.map +1 -1
  25. package/dist/client/v2-e2ee.js +16 -8
  26. package/dist/client/v2-e2ee.js.map +1 -1
  27. package/dist/client.js +90 -51
  28. package/dist/client.js.map +1 -1
  29. package/dist/ownership-v21.d.ts +2 -1
  30. package/dist/ownership-v21.js +19 -9
  31. package/dist/ownership-v21.js.map +1 -1
  32. package/dist/pubsub/binding.d.ts +2 -6
  33. package/dist/pubsub/binding.js +58 -61
  34. package/dist/pubsub/binding.js.map +1 -1
  35. package/dist/pubsub/channel.js +3 -3
  36. package/dist/pubsub/channel.js.map +1 -1
  37. package/dist/pubsub/client.js +3 -4
  38. package/dist/pubsub/client.js.map +1 -1
  39. package/dist/pubsub/service.js +1 -1
  40. package/dist/pubsub/service.js.map +1 -1
  41. package/dist/secret-store/file-store.js +8 -1
  42. package/dist/secret-store/file-store.js.map +1 -1
  43. package/dist/tools/cross-sdk-agent.d.ts +1 -1
  44. package/dist/udp-standby.d.ts +0 -1
  45. package/dist/udp-standby.js +10 -13
  46. package/dist/udp-standby.js.map +1 -1
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.js +1 -1
  49. package/package.json +1 -1
package/dist/client.js CHANGED
@@ -231,9 +231,32 @@ function clampReconnectDelayMs(value, fallback, upper = RECONNECT_MAX_BASE_DELAY
231
231
  return Math.min(Math.max(ms, RECONNECT_MIN_BASE_DELAY_MS), upper);
232
232
  }
233
233
  function reconnectSleepDelayMs(baseDelay, maxBaseDelay) {
234
- // 抖动只随当前退避基数增长,并限制在 maxBaseDelay 内。
235
- const jitterLimit = Math.min(Math.max(baseDelay, 0), Math.max(maxBaseDelay - baseDelay, 0));
236
- return baseDelay + Math.random() * jitterLimit;
234
+ return Math.max(1, Math.random() * Math.min(baseDelay, maxBaseDelay));
235
+ }
236
+ const reconnectSlots = new Map();
237
+ async function acquireReconnectSlot(aid, wait, pause) {
238
+ const issuer = aid.slice(aid.indexOf('.') + 1).toLowerCase();
239
+ let slots = reconnectSlots.get(issuer);
240
+ if (!slots) {
241
+ slots = { active: new Set(), next: 0 };
242
+ reconnectSlots.set(issuer, slots);
243
+ }
244
+ while (slots.active.size >= 2)
245
+ await wait(Promise.race(slots.active));
246
+ let done;
247
+ const ticket = new Promise(resolve => { done = resolve; });
248
+ slots.active.add(ticket);
249
+ const release = () => { slots.active.delete(ticket); done(); };
250
+ const delay = Math.min(500, Math.max(0, slots.next - Date.now()));
251
+ slots.next = Date.now() + delay + 250 + Math.random() * 250;
252
+ try {
253
+ await pause(delay);
254
+ return release;
255
+ }
256
+ catch (error) {
257
+ release();
258
+ throw error;
259
+ }
237
260
  }
238
261
  /** peer 证书缓存 TTL(1 小时) */
239
262
  const PEER_CERT_CACHE_TTL = 3600;
@@ -743,7 +766,7 @@ export class AUNClient {
743
766
  if (current())
744
767
  this._credentialRenewalTimer = setTimeout(() => { void scan(); }, 60_000);
745
768
  };
746
- void scan();
769
+ this._credentialRenewalTimer = setTimeout(() => { void scan(); }, 30_000 + Math.random() * 30_000);
747
770
  }
748
771
  _stopCredentialRenewal() {
749
772
  this._credentialRenewalAbort?.abort();
@@ -1415,25 +1438,30 @@ export class AUNClient {
1415
1438
  const observation = { root: manager.root, etag: observedEtag, version: Number(record?.observation_version ?? 0), content_hash: typeof record?.content === 'string' ? ownershipSha256(record.content) : '' };
1416
1439
  const cache = this._ownershipConsensusCache ??= new Map();
1417
1440
  const cached = cache.get(target);
1418
- if (!force && cached?.result.status === 'consensus' && record?.remote_status !== 'missing'
1441
+ if (cached?.result.status === 'consensus' && cached.observedVersion === observation.version && record?.remote_status !== 'missing'
1442
+ && (!record?.remote_owner_md_sha256 || record.remote_owner_md_sha256 === cached.result.owner_md_sha256)
1419
1443
  && (observedEtag === cached.observedEtag || observedEtag === cached.result.agent_md_etag)
1420
1444
  && (typeof record?.content !== 'string' || ownershipSha256(record.content) === cached.result.agent_md_sha256)) {
1421
1445
  cached.observedEtag = observedEtag;
1422
1446
  return { ...cached.result };
1423
1447
  }
1424
- if (!force && !cached) {
1448
+ if (!cached) {
1425
1449
  const local = manager.localOwnershipConsensus(target, record);
1426
1450
  if (local) {
1427
- cache.set(target, { observedEtag, result: local });
1451
+ cache.set(target, { observedEtag, observedVersion: observation.version, result: local });
1428
1452
  return { ...local };
1429
1453
  }
1430
1454
  }
1431
1455
  cache.delete(target);
1432
1456
  const result = { status: 'unavailable', sub_aid: target, owner_aid: null, relation_version: 0, lock: 'unlocked', agent_md_etag: '', agent_md_sha256: '' };
1433
- const entry = { observedEtag, result: { ...result } };
1457
+ const entry = { observedEtag, observedVersion: observation.version, result: { ...result } };
1434
1458
  cache.set(target, entry);
1459
+ let consensusContent;
1435
1460
  try {
1436
1461
  const snapshot = await manager.readOwnershipFresh(target);
1462
+ consensusContent = snapshot.content;
1463
+ if (snapshot.owner_md_sha256)
1464
+ result.owner_md_sha256 = snapshot.owner_md_sha256;
1437
1465
  Object.assign(result, { relation_version: snapshot.relation_version, lock: snapshot.lock, agent_md_etag: snapshot.etag,
1438
1466
  agent_md_sha256: snapshot.exists ? ownershipSha256(snapshot.content) : '' });
1439
1467
  if (snapshot.ownership_status)
@@ -1454,11 +1482,15 @@ export class AUNClient {
1454
1482
  result.retryable = result.status === 'unavailable';
1455
1483
  }
1456
1484
  if (this._agentMdManager === manager && this._ownershipConsensusCache === cache && cache.get(target) === entry) {
1457
- entry.result = { ...result };
1458
1485
  try {
1459
- manager.cacheOwnershipSnapshot(target, result, observation);
1486
+ if (manager.cacheOwnershipSnapshot(target, result, observation, consensusContent))
1487
+ entry.result = { ...result };
1488
+ else
1489
+ cache.delete(target);
1490
+ }
1491
+ catch {
1492
+ cache.delete(target);
1460
1493
  }
1461
- catch { }
1462
1494
  }
1463
1495
  return result;
1464
1496
  }
@@ -2118,6 +2150,7 @@ export class AUNClient {
2118
2150
  throw ownershipFailure('conflict', 'entity lifecycle ownership action changed');
2119
2151
  const ownership = { statement: declaration.statement, action, ...(transfer ? { transfer: { new_owner_aid: ownershipAid(newOwner, 'new_owner_aid'), target_relation_version: declaration.statement.relation_version + 1, expires_at: Number(params.expires_at ?? Math.floor(Date.now() / 1000) + 3600) } } : {}) };
2120
2152
  const raw = signOwnershipFile(owner, `---\nowner_aid: ${owner.aid}\nsub_aid: ${target}\nownership: ${JSON.stringify(ownership)}\n---\n`, 'owner');
2153
+ await this._getOwnershipFiles().verifyFile(raw, 'owner', target, owner.aid, true);
2121
2154
  await storage.writeOwnerMd(owner.aid, target, raw, original, assertCurrent);
2122
2155
  return async (error) => {
2123
2156
  if (!(error instanceof AUNError) || error instanceof TimeoutError || error instanceof ConnectionError || error.retryable || isJsonObject(error.data) && error.data.committed === true || ![-32602, 4000, 4030, 403].includes(error.code) && !['INVALID_ARGUMENT', 'ROLE_ACCESS_DENIED', 'MEMBER_NOT_FOUND', 'permission_denied', 'member_banned', 'member_blocked'].includes(error.stringCode))
@@ -2263,12 +2296,14 @@ export class AUNClient {
2263
2296
  else {
2264
2297
  const payload = `---\nowner_aid: ${owner.aid}\nsub_aid: ${aid}\nownership: ${JSON.stringify({ statement, action: 'declare' })}\n---\n`;
2265
2298
  const raw = signOwnershipFile(owner, payload, 'owner');
2299
+ await this._getOwnershipFiles().verifyFile(raw, 'owner', aid, owner.aid, true);
2266
2300
  assertCurrent();
2267
2301
  await storage.writeOwnerMd(owner.aid, aid, raw, original, assertCurrent);
2268
2302
  }
2269
2303
  const source = content ?? (observed.exists ? observed.content : buildDefaultAgentMd(aid, { type, name: String(entity.name ?? aid) }));
2270
2304
  const updated = updateOwnershipAgentMd(source, { owner: owner.aid, ownership: { statement } }).replace(/^entity_cert_fingerprint:.*(?:\r?\n|$)/m, '');
2271
2305
  const signed = signOwnershipFile(local.data.aid, updated, 'agent');
2306
+ await this._getOwnershipFiles().verifyFile(signed, 'agent', aid, undefined, true);
2272
2307
  if (content === undefined) {
2273
2308
  if (!previous || previous.owner_aid !== owner.aid || previous.relation_version !== version) {
2274
2309
  assertCurrent();
@@ -2463,28 +2498,43 @@ export class AUNClient {
2463
2498
  tokens.saveMetadata(orgAid, { org_identity_creation: null });
2464
2499
  return checkpoint.result;
2465
2500
  }
2466
- _schedulePostConnectRecovery() {
2501
+ _schedulePostConnectRecovery(backgroundSync = this._sessionOptions.background_sync !== false) {
2467
2502
  this._postConnectRecoveryAbort?.abort();
2468
2503
  if (this._postConnectRecoveryTimer !== null)
2469
2504
  clearTimeout(this._postConnectRecoveryTimer);
2470
2505
  this._postConnectRecoveryTimer = null;
2471
2506
  const store = this._aidStore;
2472
- if (this._sessionOptions.background_sync === false)
2473
- return;
2474
2507
  const abort = new AbortController();
2475
2508
  this._postConnectRecoveryAbort = abort;
2476
2509
  const aid = this._aid, version = this._serviceEventIdentityVersion, connection = this._entityOwnershipConnection;
2477
2510
  const current = () => !abort.signal.aborted && aid === this._aid && version === this._serviceEventIdentityVersion && connection === this._entityOwnershipConnection && ['ready', 'connected'].includes(this._state) && !this._closing && !this._disconnecting;
2511
+ const tasks = [];
2512
+ if (!this._ownershipOperation)
2513
+ tasks.push(() => this._checkIdentityAdmissionAfterConnect());
2514
+ tasks.push(() => this._reconcileReadStates());
2515
+ if (backgroundSync) {
2516
+ tasks.push(() => this._recoverGroupInvites());
2517
+ if (this._v2Session && this._sessionOptions.connection_kind !== 'short')
2518
+ tasks.push(() => this._v2AutoConfirmPendingProposals());
2519
+ tasks.push(() => this._delivery.restoreMessageClassPulls(), () => this._fillP2pGap());
2520
+ if (store && !this._ownershipOperation)
2521
+ tasks.push(() => this._resumeOrgIdentityAfterConnect(store, current));
2522
+ }
2523
+ const run = async () => {
2524
+ for (let index = 0; index < tasks.length; index++) {
2525
+ if (!current())
2526
+ return;
2527
+ const pending = tasks[index]();
2528
+ this._safeAsync(pending);
2529
+ await pending.catch(() => { });
2530
+ if (index + 1 < tasks.length)
2531
+ await this._sleep(1000 + Math.random() * 4000, abort.signal);
2532
+ }
2533
+ };
2478
2534
  this._postConnectRecoveryTimer = setTimeout(() => {
2479
2535
  this._postConnectRecoveryTimer = null;
2480
- if (!current())
2481
- return;
2482
- this._safeAsync(this._delivery.restoreMessageClassPulls());
2483
- void this._fillP2pGap().catch((exc) => {
2484
- this._clientLog.warn(`schedule post-connect P2P gap fill failed: ${formatCaughtError(exc)}`);
2485
- });
2486
- if (store && !this._ownershipOperation)
2487
- this._safeAsync(this._resumeOrgIdentityAfterConnect(store, current));
2536
+ if (current())
2537
+ this._safeAsync(run());
2488
2538
  }, 30_000 + Math.random() * 30_000);
2489
2539
  this._postConnectRecoveryTimer.unref?.();
2490
2540
  }
@@ -4978,18 +5028,11 @@ export class AUNClient {
4978
5028
  }
4979
5029
  this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
4980
5030
  this._startBackgroundTasks();
4981
- this._safeAsync(this._reconcileReadStates());
4982
5031
  const connectionKind = String(params.connection_kind ?? 'long');
4983
5032
  const isShortConnection = connectionKind === 'short';
4984
5033
  const hasExplicitBackgroundSync = Object.prototype.hasOwnProperty.call(params, 'background_sync');
4985
5034
  const backgroundSyncEnabled = this._sessionOptions.background_sync !== false
4986
5035
  && (!isShortConnection || hasExplicitBackgroundSync);
4987
- if (backgroundSyncEnabled) {
4988
- // 连接建立/恢复后先从事实列表恢复定向邀请提示;失败不影响连接本身。
4989
- this._safeAsync(this._recoverGroupInvites().catch((exc) => {
4990
- this._clientLog.debug(`group invite recovery failed: ${formatCaughtError(exc)}`);
4991
- }));
4992
- }
4993
5036
  if (!isShortConnection) {
4994
5037
  await this._v2E2EE.onConnected({ backgroundSync: backgroundSyncEnabled });
4995
5038
  this._assertReconnectOwner(reconnectOwner);
@@ -5004,13 +5047,7 @@ export class AUNClient {
5004
5047
  // 群消息按惰性触发,不在此处主动 pull
5005
5048
  this._entityOwnershipConnection++;
5006
5049
  this._entityOwnershipChecks.clear();
5007
- if (!this._ownershipOperation)
5008
- this._safeAsync(this._checkIdentityAdmissionAfterConnect());
5009
- if (backgroundSyncEnabled)
5010
- this._schedulePostConnectRecovery();
5011
- if (!isShortConnection && backgroundSyncEnabled && this._v2Session) {
5012
- this._safeAsync(this._v2AutoConfirmPendingProposals());
5013
- }
5050
+ this._schedulePostConnectRecovery(backgroundSyncEnabled);
5014
5051
  this._activateUdpStandby();
5015
5052
  this._clientLog.debug(`_connectOnce exit: elapsed=${Date.now() - tStart}ms gateway=${gatewayUrl}, aid=${this._aid ?? ''}`);
5016
5053
  }
@@ -6451,8 +6488,17 @@ export class AUNClient {
6451
6488
  reconnect: async () => {
6452
6489
  if (this._closing || this._disconnecting)
6453
6490
  throw new ConnectionError('client closed');
6454
- this._state = 'standby';
6455
- await this._lifecycle.connect(this._udpStandbyConnectOptions);
6491
+ await this._sleep(reconnectSleepDelayMs(1000, RECONNECT_MAX_BASE_DELAY_MS));
6492
+ const release = await acquireReconnectSlot(this._aid ?? '', task => task, ms => this._sleep(ms));
6493
+ try {
6494
+ if (this._closing || this._disconnecting || this._udpStandby?.state === 'offline')
6495
+ throw new ConnectionError('client closed');
6496
+ this._state = 'standby';
6497
+ await this._lifecycle.connect(this._udpStandbyConnectOptions);
6498
+ }
6499
+ finally {
6500
+ release();
6501
+ }
6456
6502
  },
6457
6503
  });
6458
6504
  this._udpStandby.activate(this._udpStandbyPolicy);
@@ -6580,9 +6626,10 @@ export class AUNClient {
6580
6626
  next_retry_at: this._nextRetryAt,
6581
6627
  }))
6582
6628
  return;
6629
+ let releaseReconnectSlot;
6583
6630
  try {
6584
- // 抖动随当前 base 增长且不超过 max_base,避免首次重连被放大到几十秒。
6585
6631
  await this._sleep(Math.max(0, this._nextRetryAt - Date.now()), reconnectAbort?.signal);
6632
+ releaseReconnectSlot = await acquireReconnectSlot(this._aid ?? '', task => this._lifecycle.waitForAttempt(task, undefined, reconnectAbort ?? undefined), ms => this._sleep(ms, reconnectAbort?.signal));
6586
6633
  if (!this._ownsReconnect(reconnectAbort))
6587
6634
  return;
6588
6635
  this._state = 'reconnecting';
@@ -6591,17 +6638,6 @@ export class AUNClient {
6591
6638
  attempt,
6592
6639
  }))
6593
6640
  return;
6594
- // /health 仅作旁路诊断,不能阻止实际 WebSocket 重连。
6595
- if (this._gatewayUrl) {
6596
- void this._discovery.checkHealth(this._gatewayUrl, 5_000)
6597
- .then((healthy) => {
6598
- if (!healthy)
6599
- this._clientLog.warn(`reconnect health advisory failed: gateway=${this._gatewayUrl}`);
6600
- })
6601
- .catch((exc) => {
6602
- this._clientLog.debug(`reconnect health advisory error: ${formatCaughtError(exc)}`);
6603
- });
6604
- }
6605
6641
  await this._transport.close();
6606
6642
  if (!this._ownsReconnect(reconnectAbort))
6607
6643
  return;
@@ -6638,7 +6674,7 @@ export class AUNClient {
6638
6674
  this._sessionParams.access_token = '';
6639
6675
  }
6640
6676
  }
6641
- const candidates = await this._lifecycle.waitForAttempt(this._resolveGatewayCandidatesForAid(this._aid, true, reconnectAbort ?? undefined), undefined, reconnectAbort ?? undefined);
6677
+ const candidates = await this._lifecycle.waitForAttempt(this._resolveGatewayCandidatesForAid(this._aid, this._migrationPending || this._migrationRevision.size > 0 || attempt % 3 === 2, reconnectAbort ?? undefined), undefined, reconnectAbort ?? undefined);
6642
6678
  if (!this._ownsReconnect(reconnectAbort))
6643
6679
  return;
6644
6680
  this._sessionParams.gateways = [...candidates];
@@ -6698,6 +6734,9 @@ export class AUNClient {
6698
6734
  }
6699
6735
  delay = Math.min(delay * 2, maxBaseDelay);
6700
6736
  }
6737
+ finally {
6738
+ releaseReconnectSlot?.();
6739
+ }
6701
6740
  }
6702
6741
  }
6703
6742
  /** 每轮重连从当前地址之后开始轮转,避免永远黏在失效的首个 Gateway。 */