@agentunion/fastaun 0.5.34 → 0.5.35

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 (78) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/_packed_docs/CHANGELOG.md +30 -1
  3. package/_packed_docs/INDEX.md +11 -11
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +9 -9
  5. package/_packed_docs/agent.md/SCHEMA.md +1 -1
  6. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +2 -2
  7. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +17 -17
  8. package/_packed_docs/protocol/echo-probe-gateway.md +99 -0
  9. package/_packed_docs/protocol/echo-probe-sdk.md +91 -0
  10. package/_packed_docs/sdk/0.5.35-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +189 -0
  11. package/_packed_docs/sdk/0.5.35-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +204 -0
  12. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +2 -2
  13. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +32 -6
  14. package/_packed_docs/sdk/09-group-rpc-manual.md +7 -7
  15. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +13 -13
  16. package/_packed_docs/sdk/AUN/344/272/213/344/273/266/345/217/221/345/270/203/350/256/242/351/230/205/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +73 -3
  17. package/_packed_docs/sdk/AUN/344/272/213/344/273/266/345/217/221/345/270/203/350/256/242/351/230/205/345/274/200/345/217/221/347/247/273/346/244/215/346/214/207/345/215/227.md +50 -8
  18. package/_packed_docs/sdk/INDEX.md +18 -18
  19. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +1 -1
  20. package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +5 -5
  21. package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +5 -5
  22. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +9 -9
  23. package/_packed_docs//346/266/210/346/201/257/345/274/271/346/200/247/350/241/245/346/213/211/351/200/232/351/201/223/345/256/236/346/226/275/350/256/241/345/210/222.md +400 -0
  24. package/_packed_docs//346/266/210/346/201/257/345/274/271/346/200/247/350/241/245/346/213/211/351/200/232/351/201/223/346/226/271/346/241/210.md +485 -0
  25. package/dist/auth.js +31 -43
  26. package/dist/auth.js.map +1 -1
  27. package/dist/client/delivery.d.ts +7 -1
  28. package/dist/client/delivery.js +137 -16
  29. package/dist/client/delivery.js.map +1 -1
  30. package/dist/client/lifecycle.js +10 -1
  31. package/dist/client/lifecycle.js.map +1 -1
  32. package/dist/client/rpc-pipeline.d.ts +2 -0
  33. package/dist/client/rpc-pipeline.js +65 -7
  34. package/dist/client/rpc-pipeline.js.map +1 -1
  35. package/dist/client/sync-channels.d.ts +2 -1
  36. package/dist/client/sync-channels.js +33 -14
  37. package/dist/client/sync-channels.js.map +1 -1
  38. package/dist/client/v2-e2ee.d.ts +1 -0
  39. package/dist/client/v2-e2ee.js +19 -5
  40. package/dist/client/v2-e2ee.js.map +1 -1
  41. package/dist/client.d.ts +15 -0
  42. package/dist/client.js +310 -49
  43. package/dist/client.js.map +1 -1
  44. package/dist/events.d.ts +1 -0
  45. package/dist/events.js +5 -0
  46. package/dist/events.js.map +1 -1
  47. package/dist/index.d.ts +2 -0
  48. package/dist/index.js +1 -0
  49. package/dist/index.js.map +1 -1
  50. package/dist/mail-client.d.ts +101 -0
  51. package/dist/mail-client.js +369 -0
  52. package/dist/mail-client.js.map +1 -0
  53. package/dist/pubsub/binding.js +9 -2
  54. package/dist/pubsub/binding.js.map +1 -1
  55. package/dist/pubsub/channel.d.ts +11 -3
  56. package/dist/pubsub/channel.js +88 -7
  57. package/dist/pubsub/channel.js.map +1 -1
  58. package/dist/pubsub/client.d.ts +8 -2
  59. package/dist/pubsub/client.js +59 -17
  60. package/dist/pubsub/client.js.map +1 -1
  61. package/dist/pubsub/index.d.ts +2 -2
  62. package/dist/pubsub/index.js +1 -1
  63. package/dist/pubsub/index.js.map +1 -1
  64. package/dist/pubsub/service.d.ts +6 -0
  65. package/dist/pubsub/service.js +100 -9
  66. package/dist/pubsub/service.js.map +1 -1
  67. package/dist/pubsub/types.d.ts +11 -1
  68. package/dist/tools/cross-sdk-agent.d.ts +1 -1
  69. package/dist/transport.d.ts +4 -0
  70. package/dist/transport.js +39 -2
  71. package/dist/transport.js.map +1 -1
  72. package/dist/udp-standby.d.ts +89 -0
  73. package/dist/udp-standby.js +487 -0
  74. package/dist/udp-standby.js.map +1 -0
  75. package/dist/version.d.ts +1 -1
  76. package/dist/version.js +1 -1
  77. package/package.json +5 -1
  78. package/scripts/patch-imapflow-external.mjs +69 -0
package/dist/client.d.ts CHANGED
@@ -32,6 +32,7 @@ import type { SemanticPersistentState } from './semantic-facade.js';
32
32
  */
33
33
  export declare function stableStringify(obj: JsonValue | object | undefined): string;
34
34
  export interface ConnectionOptions {
35
+ udp_standby?: boolean;
35
36
  auto_reconnect?: boolean;
36
37
  connect_timeout?: number;
37
38
  retry?: JsonObject;
@@ -143,6 +144,9 @@ export declare class AUNClient {
143
144
  private _discovery;
144
145
  /** 传输层 */
145
146
  private _transport;
147
+ private _udpStandby;
148
+ private _udpStandbyPolicy;
149
+ private _udpStandbyConnectOptions;
146
150
  /** 认证流程 */
147
151
  private _auth;
148
152
  resolveCredentialService?: (cid: string) => string | null | undefined | Promise<string | null | undefined>;
@@ -166,6 +170,8 @@ export declare class AUNClient {
166
170
  private _groupIdentityOperations;
167
171
  private _entityOwnershipConnection;
168
172
  private _entityOwnershipChecks;
173
+ private _postConnectRecoveryTimer;
174
+ private _postConnectRecoveryAbort;
169
175
  private _groupIndexMetaCache;
170
176
  /** 消息序列号跟踪器(群消息 + P2P 空洞检测) */
171
177
  private _p2pPeerPullEnabled;
@@ -423,6 +429,7 @@ export declare class AUNClient {
423
429
  private _resumeOrgRequest;
424
430
  createOrg(params?: RpcParams, options?: OrgIdentityOptions): Promise<RpcResult>;
425
431
  private _createOrg;
432
+ private _schedulePostConnectRecovery;
426
433
  private _resumeOrgIdentityAfterConnect;
427
434
  private _verifyOrgTransferMaterials;
428
435
  completeOrgTransfer(params?: RpcParams, options?: OrgIdentityOptions): Promise<RpcResult>;
@@ -483,10 +490,17 @@ export declare class AUNClient {
483
490
  private _markPublishedSeq;
484
491
  private _publishAppEvent;
485
492
  private _echoTimestamp;
493
+ private _echoStarts;
486
494
  private _isEchoPayload;
495
+ private _echoHop;
496
+ private _isEchoReturn;
497
+ private _appendEchoTrace;
487
498
  private _maybeAppendEchoTraceSend;
488
499
  private _shouldSkipEventSignature;
489
500
  private _maybeAppendEchoTraceReceive;
501
+ private _returnReceivedEcho;
502
+ private _echoFailure;
503
+ private _publishEchoReturn;
490
504
  private _debugJson;
491
505
  private _messageEnvelopeFieldsForDebug;
492
506
  private _isMessageDebugEnabled;
@@ -680,6 +694,7 @@ export declare class AUNClient {
680
694
  private _onGatewayDisconnect;
681
695
  /** 传输层断线回调 */
682
696
  private _handleTransportDisconnect;
697
+ private _activateUdpStandby;
683
698
  /** 启动重连循环(默认无限重试 + 当前基数抖动,仅在不可重试错误、close() 或 max_attempts 耗尽时终止) */
684
699
  private _startReconnect;
685
700
  private _ownsReconnect;
package/dist/client.js CHANGED
@@ -26,6 +26,7 @@ import { AUNLogger } from './logger.js';
26
26
  import { normalizeGroupId } from './group-id.js';
27
27
  import { validateAIDFormat, validateGroupIDFormat } from './validators.js';
28
28
  import { RPCTransport } from './transport.js';
29
+ import { UdpStandbyController } from './udp-standby.js';
29
30
  import { SyncChannels } from './client/sync-channels.js';
30
31
  import { AuthFlow } from './auth.js';
31
32
  import { AgentMdManager, buildDefaultAgentMd, updateOwnershipAgentMd } from './agent-md.js';
@@ -909,6 +910,9 @@ export class AUNClient {
909
910
  _discovery;
910
911
  /** 传输层 */
911
912
  _transport;
913
+ _udpStandby = null;
914
+ _udpStandbyPolicy;
915
+ _udpStandbyConnectOptions;
912
916
  /** 认证流程 */
913
917
  _auth;
914
918
  resolveCredentialService;
@@ -932,6 +936,8 @@ export class AUNClient {
932
936
  _groupIdentityOperations = new Map();
933
937
  _entityOwnershipConnection = 0;
934
938
  _entityOwnershipChecks = new Map();
939
+ _postConnectRecoveryTimer = null;
940
+ _postConnectRecoveryAbort = null;
935
941
  _groupIndexMetaCache = new GroupIndexMetaCache();
936
942
  /** 消息序列号跟踪器(群消息 + P2P 空洞检测) */
937
943
  _p2pPeerPullEnabled = true;
@@ -1598,6 +1604,10 @@ export class AUNClient {
1598
1604
  }
1599
1605
  _applyAidRuntimeContext(aid) {
1600
1606
  this._aidStore = aid._aidStore;
1607
+ this._udpStandby?.close();
1608
+ this._udpStandby = null;
1609
+ this._udpStandbyPolicy = undefined;
1610
+ this._udpStandbyConnectOptions = undefined;
1601
1611
  const oldTransport = this._transport;
1602
1612
  try {
1603
1613
  const closeResult = oldTransport?.close?.();
@@ -1850,10 +1860,14 @@ export class AUNClient {
1850
1860
  }
1851
1861
  /** 连接到 Gateway;身份来自构造函数或 loadIdentity(aid),认证由 SDK 内部自动完成。 */
1852
1862
  async connect(opts) {
1863
+ if (this._udpStandby && ['standby', 'preparing', 'waking'].includes(this._udpStandby.state))
1864
+ return this._udpStandby.ensureActive();
1865
+ this._udpStandbyConnectOptions = opts ? { ...opts } : undefined;
1853
1866
  await this._lifecycle.connect(opts);
1854
1867
  }
1855
1868
  /** 关闭连接 */
1856
1869
  async close() {
1870
+ this._udpStandby?.close();
1857
1871
  this._risk.close();
1858
1872
  this._peerGatewayCache.clear();
1859
1873
  return this._lifecycle.close();
@@ -1863,6 +1877,7 @@ export class AUNClient {
1863
1877
  * disconnect 是可恢复的:停止心跳、关闭 WebSocket,但不清理 keystore 等状态。
1864
1878
  */
1865
1879
  async disconnect() {
1880
+ this._udpStandby?.close();
1866
1881
  return this._lifecycle.disconnect();
1867
1882
  }
1868
1883
  get riskState() { return Object.freeze(structuredClone(this._risk.state)); }
@@ -2020,27 +2035,47 @@ export class AUNClient {
2020
2035
  * 自动处理内部方法限制、E2EE 加解密、客户端签名等。
2021
2036
  */
2022
2037
  async call(method, params, options) {
2023
- const assertCurrent = ['group.transfer_owner', 'org.lifecycle.transfer', 'group.dissolve', 'org.lifecycle.dissolve'].includes(method) ? this._entityOwnershipGuard() : undefined;
2024
- const recoverOwnership = ['group.transfer_owner', 'org.lifecycle.transfer', 'group.dissolve', 'org.lifecycle.dissolve'].includes(method) ? await this._prepareEntityOwnershipLifecycle(method, params ?? {}) : undefined;
2025
- await this._checkEntityOwnershipOnAccess(method, params ?? {});
2026
- const readIdentity = ['message.read', 'message.read_state', 'group.read', 'group.read_state'].includes(method)
2027
- ? this._ensureReadStateIdentity() : null;
2028
- try {
2029
- const result = await (options === undefined ? this._rpcPipeline.call(method, params, assertCurrent) : this._rpcPipeline.call(method, params, assertCurrent, options));
2030
- await this._safeAsync(this._getCredentialRenewal().observe(method, params ?? {}, result));
2031
- if (readIdentity !== null && readIdentity === this._ensureReadStateIdentity())
2032
- this._observeReadStateResult(method, result, params ?? {});
2033
- return result;
2038
+ const echoStarted = performance.now();
2039
+ if ((method === 'message.send' || method === 'group.send') && params && this._isEchoPayload(params.payload ?? params.content)) {
2040
+ params = { ...params };
2041
+ this._echoStarts.set(params, echoStarted);
2042
+ }
2043
+ const lifecycle = ['group.transfer_owner', 'org.lifecycle.transfer', 'group.dissolve', 'org.lifecycle.dissolve'].includes(method);
2044
+ let standbyRelease;
2045
+ if (lifecycle && this._udpStandby) {
2046
+ const deadline = Math.min(options?.deadline ?? Infinity, typeof params?._rpc_deadline === 'number' ? params._rpc_deadline : Infinity, Date.now() + Number(this._sessionOptions?.timeouts?.call ?? 35) * 1000);
2047
+ standbyRelease = await this._udpStandby.acquire(method, deadline);
2048
+ options = { ...options, deadline };
2034
2049
  }
2035
- catch (err) {
2036
- if (recoverOwnership) {
2037
- try {
2038
- await recoverOwnership(err);
2050
+ try {
2051
+ const assertCurrent = lifecycle ? this._entityOwnershipGuard() : undefined;
2052
+ const recoverOwnership = lifecycle ? await this._prepareEntityOwnershipLifecycle(method, params ?? {}) : undefined;
2053
+ await this._checkEntityOwnershipOnAccess(method, params ?? {});
2054
+ const readIdentity = ['message.read', 'message.read_state', 'group.read', 'group.read_state'].includes(method)
2055
+ ? this._ensureReadStateIdentity() : null;
2056
+ try {
2057
+ const result = await (options === undefined ? this._rpcPipeline.call(method, params, assertCurrent) : this._rpcPipeline.call(method, params, assertCurrent, options));
2058
+ await this._safeAsync(this._getCredentialRenewal().observe(method, params ?? {}, result));
2059
+ if (readIdentity !== null && readIdentity === this._ensureReadStateIdentity())
2060
+ this._observeReadStateResult(method, result, params ?? {});
2061
+ return result;
2062
+ }
2063
+ catch (err) {
2064
+ if (recoverOwnership) {
2065
+ try {
2066
+ await recoverOwnership(err);
2067
+ }
2068
+ catch { }
2039
2069
  }
2040
- catch { }
2070
+ await this._maybeHandleHalfOpenRpcFailure(err);
2071
+ const returned = await this._echoFailure(method, params, err);
2072
+ if (returned)
2073
+ return returned;
2074
+ throw err;
2041
2075
  }
2042
- await this._maybeHandleHalfOpenRpcFailure(err);
2043
- throw err;
2076
+ }
2077
+ finally {
2078
+ standbyRelease?.();
2044
2079
  }
2045
2080
  }
2046
2081
  _entityOwnershipGuard() {
@@ -2268,9 +2303,27 @@ export class AUNClient {
2268
2303
  const groupId = String(params.group_aid ?? params.groupAid ?? params.group_id ?? params.groupId ?? message.slice(prefix.length)).trim();
2269
2304
  const target = normalizeGroupId(groupId.toLowerCase(), { localIssuer: this._aid?.split('.').slice(1).join('.') });
2270
2305
  const key = `${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}:${target}`;
2271
- if (!this._entityOwnershipChecks.has(key))
2272
- await this._checkEntityOwnershipOnAccess('group.get_info', { group_id: groupId });
2273
- return await this._entityOwnershipChecks.get(key) ?? false;
2306
+ const assertCurrent = this._entityOwnershipGuard();
2307
+ try {
2308
+ assertCurrent();
2309
+ const pending = this._groupIdentityOperations.get(key);
2310
+ if (pending && !['group.create', 'group.bind_group_aid'].includes(_method)) {
2311
+ const local = this._aidStore?.load(target);
2312
+ if (!local?.ok || !local.data.aid.isPrivateKeyValid()) {
2313
+ await pending.task;
2314
+ assertCurrent();
2315
+ return true;
2316
+ }
2317
+ }
2318
+ if (!this._entityOwnershipChecks.has(key))
2319
+ await this._checkEntityOwnershipOnAccess('group.get_info', { group_id: groupId });
2320
+ const recovered = await this._entityOwnershipChecks.get(key) ?? false;
2321
+ assertCurrent();
2322
+ return recovered;
2323
+ }
2324
+ catch {
2325
+ return false;
2326
+ }
2274
2327
  }
2275
2328
  }
2276
2329
  const agentPrefix = 'valid agent.md is required for ';
@@ -2319,17 +2372,14 @@ export class AUNClient {
2319
2372
  catch (exc) {
2320
2373
  this._clientLog.warn(`post-connect agent.md check failed: ${formatCaughtError(exc)}`);
2321
2374
  }
2322
- const store = this._aidStore;
2323
- if (!store)
2324
- return;
2325
- await this._resumeOrgIdentityAfterConnect(store);
2326
2375
  }
2327
2376
  async _runGroupIdentityOperation(groupId, operation, context = '') {
2328
2377
  const aid = String(this._aid ?? '').trim();
2329
2378
  const dot = aid.indexOf('.');
2330
- const key = normalizeGroupId(groupId, { localIssuer: dot >= 0 ? aid.slice(dot + 1) : '' }).toLowerCase();
2331
- if (!key)
2379
+ const target = normalizeGroupId(groupId, { localIssuer: dot >= 0 ? aid.slice(dot + 1) : '' }).toLowerCase();
2380
+ if (!target)
2332
2381
  return operation();
2382
+ const key = `${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}:${target}`;
2333
2383
  const pending = this._groupIdentityOperations.get(key);
2334
2384
  if (pending && pending.context !== context)
2335
2385
  throw ownershipFailure('conflict', 'pending Org creation context changed');
@@ -2413,12 +2463,42 @@ export class AUNClient {
2413
2463
  tokens.saveMetadata(orgAid, { org_identity_creation: null });
2414
2464
  return checkpoint.result;
2415
2465
  }
2416
- async _resumeOrgIdentityAfterConnect(store) {
2466
+ _schedulePostConnectRecovery() {
2467
+ this._postConnectRecoveryAbort?.abort();
2468
+ if (this._postConnectRecoveryTimer !== null)
2469
+ clearTimeout(this._postConnectRecoveryTimer);
2470
+ this._postConnectRecoveryTimer = null;
2471
+ const store = this._aidStore;
2472
+ if (this._sessionOptions.background_sync === false)
2473
+ return;
2474
+ const abort = new AbortController();
2475
+ this._postConnectRecoveryAbort = abort;
2476
+ const aid = this._aid, version = this._serviceEventIdentityVersion, connection = this._entityOwnershipConnection;
2477
+ const current = () => !abort.signal.aborted && aid === this._aid && version === this._serviceEventIdentityVersion && connection === this._entityOwnershipConnection && ['ready', 'connected'].includes(this._state) && !this._closing && !this._disconnecting;
2478
+ this._postConnectRecoveryTimer = setTimeout(() => {
2479
+ 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));
2488
+ }, 30_000 + Math.random() * 30_000);
2489
+ this._postConnectRecoveryTimer.unref?.();
2490
+ }
2491
+ async _resumeOrgIdentityAfterConnect(store, current = () => true) {
2492
+ if (!current())
2493
+ return;
2417
2494
  const keyStore = store._keystore;
2418
2495
  const tokens = store._tokenStore;
2419
2496
  if (typeof keyStore?.listIdentities !== 'function' || typeof tokens?.loadMetadata !== 'function')
2420
2497
  return;
2421
2498
  for (const aid of keyStore.listIdentities()) {
2499
+ await new Promise((resolve) => setTimeout(resolve, 0));
2500
+ if (!current())
2501
+ return;
2422
2502
  const metadata = typeof tokens._loadMetadataStrict === 'function' ? tokens._loadMetadataStrict(aid) : tokens.loadMetadata(aid);
2423
2503
  const creation = metadata?.org_identity_creation;
2424
2504
  const transfer = metadata?.org_identity_transfer;
@@ -3593,6 +3673,8 @@ export class AUNClient {
3593
3673
  // ── 事件自动解密管线 ──────────────────────────────────────
3594
3674
  /** 处理 transport 层推送的原始 P2P 消息 */
3595
3675
  async _onRawMessageReceived(data) {
3676
+ if (isJsonObject(data))
3677
+ this._echoStarts.set(data, performance.now());
3596
3678
  if (this._controlMessages().onPush(data))
3597
3679
  return;
3598
3680
  return this._delivery.onRawMessageReceived(data);
@@ -3602,6 +3684,8 @@ export class AUNClient {
3602
3684
  }
3603
3685
  /** 处理群组消息推送:自动解密后 re-publish */
3604
3686
  async _onRawGroupMessageCreated(data) {
3687
+ if (isJsonObject(data))
3688
+ this._echoStarts.set(data, performance.now());
3605
3689
  if (this._controlMessages().onPush(data, true))
3606
3690
  return;
3607
3691
  return this._delivery.onRawGroupMessageCreated(data);
@@ -3716,21 +3800,70 @@ export class AUNClient {
3716
3800
  const ms = String(now.getMilliseconds()).padStart(3, '0');
3717
3801
  return `${hh}:${mm}:${ss}.${ms}`;
3718
3802
  }
3803
+ _echoStarts = new WeakMap();
3719
3804
  _isEchoPayload(payload) {
3720
3805
  if (!payload || typeof payload !== 'object' || Array.isArray(payload))
3721
3806
  return false;
3722
3807
  const text = payload.text;
3723
- if (typeof text !== 'string' || text.length > 4096)
3808
+ if (typeof text !== 'string')
3724
3809
  return false;
3725
- return text.split('\n', 1)[0].toLowerCase().includes('echo');
3810
+ const first = text.split('\n', 1)[0].trimStart();
3811
+ return /^(?:echo\.(?:ecdiag|ask)(?:\s|$)|echo(?:\s|\[|$))/i.test(first);
3812
+ }
3813
+ _echoHop(payload) {
3814
+ if (Number.isInteger(payload.hop)) {
3815
+ if (!Number.isSafeInteger(payload.hop))
3816
+ throw new ValidationError('echo hop must be a JSON safe integer');
3817
+ return Number(payload.hop);
3818
+ }
3819
+ const limit = /(?:^|\s)hop=([+-]?\d+)(?=\s|$)/i.exec(payload.text.split('\n', 1)[0]);
3820
+ const hop = limit ? Number(limit[1]) : 100;
3821
+ if (!Number.isSafeInteger(hop))
3822
+ throw new ValidationError('echo hop must be a JSON safe integer');
3823
+ return hop;
3824
+ }
3825
+ _isEchoReturn(payload) {
3826
+ return this._echoHop(payload) < 0 || payload.dir === 'ret' || payload.dir !== 'fwd' && payload.text.split('\n').some(line => line.trim() === '[AUN-Echo.return]');
3827
+ }
3828
+ _appendEchoTrace(payload, stage, started, endpoint = false) {
3829
+ const previousHop = this._echoHop(payload);
3830
+ const returning = endpoint || this._isEchoReturn(payload);
3831
+ const hop = returning ? previousHop - 1 : Math.max(previousHop - 1, 0);
3832
+ if (!Number.isSafeInteger(hop))
3833
+ throw new ValidationError('echo hop decrement exceeds the JSON safe integer range');
3834
+ const now = new Date();
3835
+ const offset = -now.getTimezoneOffset();
3836
+ const tz = `${offset < 0 ? '-' : '+'}${String(Math.floor(Math.abs(offset) / 60)).padStart(2, '0')}:${String(Math.abs(offset) % 60).padStart(2, '0')}`;
3837
+ const uptime = this._connectedAt ? Math.floor((now.getTime() - this._connectedAt) / 1000) : 0;
3838
+ const duration = Math.max(0, Math.round((performance.now() - started) * 1000) / 1000);
3839
+ const trace = `${this._echoTimestamp()} [AUN-SDK.${stage}] dir=${returning ? 'ret' : 'fwd'} hop=${hop} enc=0 ver=${VERSION} epoch=${now.getTime()} tz=${tz}${returning ? ` duration_ms=${duration}` : ''} aid=${this._aid ?? '-'} conn_uptime=${uptime}s`;
3840
+ const result = { ...payload, type: 'text', hop, dir: returning ? 'ret' : 'fwd', text: payload.text.trimStart() + '\n' + trace };
3841
+ delete result.max_hop;
3842
+ delete result.rhop;
3843
+ return result;
3726
3844
  }
3727
3845
  _maybeAppendEchoTraceSend(params) {
3846
+ const reflected = params._echo_reflection === true;
3847
+ delete params._echo_reflection;
3848
+ for (const key of ['hop', 'max_hop', 'rhop', 'dir'])
3849
+ delete params[key];
3728
3850
  const payload = params.payload;
3729
- if (!this._isEchoPayload(payload))
3730
- return;
3731
- const uptime = this._connectedAt ? Math.floor((Date.now() - this._connectedAt) / 1000) : 0;
3732
- const trace = `${this._echoTimestamp()} [AUN-SDK.send] aid=${this._aid ?? '-'} conn_uptime=${uptime}s`;
3733
- params.payload = { ...payload, text: payload.text + '\n' + trace };
3851
+ if (!this._isEchoPayload(payload)) {
3852
+ if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
3853
+ const clean = { ...payload };
3854
+ for (const key of ['hop', 'max_hop', 'rhop', 'dir'])
3855
+ delete clean[key];
3856
+ params.payload = clean;
3857
+ }
3858
+ return false;
3859
+ }
3860
+ if (reflected)
3861
+ return false;
3862
+ const started = this._echoStarts.get(params) ?? performance.now();
3863
+ this._echoStarts.set(params, started);
3864
+ const next = this._appendEchoTrace(payload, 'send', started);
3865
+ params.payload = next;
3866
+ return next.dir === 'fwd' && next.hop === 0;
3734
3867
  }
3735
3868
  _shouldSkipEventSignature(event) {
3736
3869
  if (event.encrypted || event.encrypt)
@@ -3738,14 +3871,79 @@ export class AUNClient {
3738
3871
  return this._isEchoPayload(event.payload);
3739
3872
  }
3740
3873
  _maybeAppendEchoTraceReceive(msg) {
3741
- if (msg.encrypted)
3742
- return;
3874
+ if (msg.encrypted || msg.encrypt)
3875
+ return false;
3876
+ const payload = msg.payload;
3877
+ if (!this._isEchoPayload(payload) || String(payload.type ?? '').startsWith('e2ee.') || payload.ciphertext)
3878
+ return false;
3879
+ const started = this._echoStarts.get(msg) ?? performance.now();
3880
+ this._echoStarts.set(msg, started);
3881
+ const next = this._appendEchoTrace(payload, 'receive', started);
3882
+ msg.payload = next;
3883
+ return next.dir === 'fwd' && next.hop === 0;
3884
+ }
3885
+ async _returnReceivedEcho(msg) {
3743
3886
  const payload = msg.payload;
3744
- if (!this._isEchoPayload(payload))
3887
+ const sender = String(msg.from ?? msg.sender_aid ?? msg.from_aid ?? '');
3888
+ const returned = this._appendEchoTrace(payload, 'receive', this._echoStarts.get(msg) ?? performance.now(), true);
3889
+ returned.text = String(returned.text) + '\n[AUN-Echo.return]';
3890
+ const groupId = String(msg.group_id ?? '');
3891
+ await this._rpcPipeline.call('message.send', {
3892
+ to: sender, encrypt: false, _echo_reflection: true,
3893
+ ...(groupId ? { group_id: groupId } : {}),
3894
+ payload: returned,
3895
+ });
3896
+ }
3897
+ async _echoFailure(method, params, error, traced = false) {
3898
+ if (!['message.send', 'group.send'].includes(method) || !params || !this._isEchoPayload(params.payload) || !(error instanceof AUNError) || error instanceof AuthError || error._echoDeliveryAttempted)
3899
+ return;
3900
+ const payload = params.payload;
3901
+ if (String(payload.type ?? '').toLowerCase().startsWith('e2ee') || payload.ciphertext)
3902
+ return;
3903
+ const started = this._echoStarts.get(params) ?? performance.now();
3904
+ let sent;
3905
+ try {
3906
+ sent = traced ? payload : this._appendEchoTrace(payload, 'send', started);
3907
+ }
3908
+ catch {
3745
3909
  return;
3746
- const uptime = this._connectedAt ? Math.floor((Date.now() - this._connectedAt) / 1000) : 0;
3747
- const trace = `${this._echoTimestamp()} [AUN-SDK.receive] aid=${this._aid ?? '-'} conn_uptime=${uptime}s`;
3748
- msg.payload = { ...payload, text: payload.text + '\n' + trace };
3910
+ }
3911
+ const reason = error instanceof TimeoutError || error.code === -32011 ? 'timeout' : 'send_failed';
3912
+ const state = error instanceof ValidationError || error instanceof ConnectionError && /not connected|closing|closed/i.test(error.message) ? 'not_delivered' : 'unknown';
3913
+ const detail = { code: error.code, message: error.message, data: error.data };
3914
+ const result = { status: 'echo_return', message_id: params.message_id ?? `m-${crypto.randomUUID()}`, from: params.to ?? params.group_id, to: this._aid, payload: sent, reason, delivery_state: state, echo_error: detail };
3915
+ if (method === 'group.send')
3916
+ result.group_id = params.group_id;
3917
+ await this._publishEchoReturn(method, params, result);
3918
+ return result;
3919
+ }
3920
+ async _publishEchoReturn(method, params, result) {
3921
+ if (!this._isEchoPayload(result.payload))
3922
+ return;
3923
+ const payload = result.payload;
3924
+ const returnedPayload = this._appendEchoTrace(payload, 'receive', this._echoStarts.get(params) ?? performance.now(), !this._isEchoReturn(payload));
3925
+ if (result.echo_error)
3926
+ returnedPayload.text += ` reason=${result.reason} delivery_state=${result.delivery_state} error=${JSON.stringify(result.echo_error)}`;
3927
+ if (!String(returnedPayload.text).split('\n').some(line => line.trim() === '[AUN-Echo.return]'))
3928
+ returnedPayload.text += '\n[AUN-Echo.return]';
3929
+ result.payload = returnedPayload;
3930
+ if (this._state === 'closed')
3931
+ return;
3932
+ const groupId = String(result.group_id ?? params.group_id ?? params.group_aid ?? '');
3933
+ const sender = String(result.from ?? result.sender_aid ?? params.to ?? this._aid ?? '');
3934
+ try {
3935
+ await this._publishAppEvent(method === 'group.send' ? 'group.message_created' : 'message.received', {
3936
+ ...result,
3937
+ message_id: String(result.message_id ?? params.message_id ?? ''),
3938
+ from: sender, sender_aid: sender, to: this._aid ?? '', encrypted: false,
3939
+ ...(groupId ? { group_id: groupId } : {}), payload: returnedPayload,
3940
+ }, 'echo_return');
3941
+ }
3942
+ catch (error) {
3943
+ if (error instanceof Error)
3944
+ error._echoDeliveryAttempted = true;
3945
+ throw error;
3946
+ }
3749
3947
  }
3750
3948
  _debugJson(value) {
3751
3949
  const seen = new WeakSet();
@@ -4637,6 +4835,10 @@ export class AUNClient {
4637
4835
  : {};
4638
4836
  const capabilities = isJsonObject(extraInfo._capabilities) ? { ...extraInfo._capabilities } : {};
4639
4837
  delete capabilities.message_classes_v1;
4838
+ if (params.udp_standby !== false && params.connection_kind !== 'short')
4839
+ capabilities.udp_standby_v1 = true;
4840
+ else
4841
+ delete capabilities.udp_standby_v1;
4640
4842
  if (typeof this._tokenStore.updateInstanceState === 'function') {
4641
4843
  capabilities.message_classes_v2 = { p2p: ['normal', 'control', 'sdk_control'], group: ['normal', 'control'] };
4642
4844
  capabilities.sdk_control_protocols = { 'aun.agent_md.changed': [1], 'aun.pubsub': [1] };
@@ -4659,12 +4861,21 @@ export class AUNClient {
4659
4861
  const challenge = await this._transport.connect(gatewayUrl);
4660
4862
  this._assertReconnectOwner(reconnectOwner);
4661
4863
  this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
4662
- const authTransport = reconnectOwner ? {
4864
+ const authTransport = {
4663
4865
  call: (...args) => {
4664
4866
  this._assertReconnectOwner(reconnectOwner);
4867
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
4665
4868
  return this._transport.call(...args);
4666
4869
  },
4667
- } : this._transport;
4870
+ connect: async (url) => {
4871
+ this._assertReconnectOwner(reconnectOwner);
4872
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
4873
+ const nextChallenge = await this._transport.connect(url);
4874
+ this._assertReconnectOwner(reconnectOwner);
4875
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
4876
+ return nextChallenge;
4877
+ },
4878
+ };
4668
4879
  this._clientLog.debug(`WebSocket connection established: gateway=${gatewayUrl}`);
4669
4880
  this._state = 'authenticating';
4670
4881
  if (allowReauth) {
@@ -4697,6 +4908,7 @@ export class AUNClient {
4697
4908
  this._bindGatewayRuntimeIdentity(auth.hello, params);
4698
4909
  this._gatewayConnectionId = isJsonObject(auth.hello) && isJsonObject(auth.hello.connection) ? String(auth.hello.connection.id ?? '') : '';
4699
4910
  this._negotiatedCapabilities = isJsonObject(auth.hello) && isJsonObject(auth.hello.negotiated_capabilities) ? auth.hello.negotiated_capabilities : {};
4911
+ this._udpStandbyPolicy = isJsonObject(auth.hello) && isJsonObject(auth.hello.udp_standby) ? auth.hello.udp_standby : undefined;
4700
4912
  if (isJsonObject(auth.hello))
4701
4913
  await this._applyRiskHello(auth.hello);
4702
4914
  }
@@ -4719,6 +4931,7 @@ export class AUNClient {
4719
4931
  this._bindGatewayRuntimeIdentity(hello, params);
4720
4932
  this._gatewayConnectionId = isJsonObject(hello) && isJsonObject(hello.connection) ? String(hello.connection.id ?? '') : '';
4721
4933
  this._negotiatedCapabilities = isJsonObject(hello) && isJsonObject(hello.negotiated_capabilities) ? hello.negotiated_capabilities : {};
4934
+ this._udpStandbyPolicy = isJsonObject(hello) && isJsonObject(hello.udp_standby) ? hello.udp_standby : undefined;
4722
4935
  if (isJsonObject(hello))
4723
4936
  await this._applyRiskHello(hello);
4724
4937
  }
@@ -4789,19 +5002,16 @@ export class AUNClient {
4789
5002
  this._agentMdChanges = new AgentMdChangeCoordinator(this);
4790
5003
  // connect/reconnect 成功后自动触发一次 P2P message.v2.pull,补齐离线期间积压
4791
5004
  // 群消息按惰性触发,不在此处主动 pull
4792
- if (backgroundSyncEnabled) {
4793
- void this._fillP2pGap().catch((exc) => {
4794
- this._clientLog.warn(`schedule post-connect P2P gap fill failed: ${formatCaughtError(exc)}`);
4795
- });
4796
- }
4797
5005
  this._entityOwnershipConnection++;
4798
5006
  this._entityOwnershipChecks.clear();
4799
5007
  if (!this._ownershipOperation)
4800
5008
  this._safeAsync(this._checkIdentityAdmissionAfterConnect());
4801
- this._safeAsync(this._delivery.restoreMessageClassPulls());
5009
+ if (backgroundSyncEnabled)
5010
+ this._schedulePostConnectRecovery();
4802
5011
  if (!isShortConnection && backgroundSyncEnabled && this._v2Session) {
4803
5012
  this._safeAsync(this._v2AutoConfirmPendingProposals());
4804
5013
  }
5014
+ this._activateUdpStandby();
4805
5015
  this._clientLog.debug(`_connectOnce exit: elapsed=${Date.now() - tStart}ms gateway=${gatewayUrl}, aid=${this._aid ?? ''}`);
4806
5016
  }
4807
5017
  catch (err) {
@@ -5724,6 +5934,11 @@ export class AUNClient {
5724
5934
  }
5725
5935
  /** 停止所有后台任务 */
5726
5936
  _stopBackgroundTasks() {
5937
+ this._postConnectRecoveryAbort?.abort();
5938
+ this._postConnectRecoveryAbort = null;
5939
+ if (this._postConnectRecoveryTimer !== null)
5940
+ clearTimeout(this._postConnectRecoveryTimer);
5941
+ this._postConnectRecoveryTimer = null;
5727
5942
  this._syncChannelCoordinator?.reset();
5728
5943
  this._stopCredentialRenewal();
5729
5944
  this._auth.invalidateStrictPkiCache();
@@ -6118,6 +6333,8 @@ export class AUNClient {
6118
6333
  }
6119
6334
  /** 传输层断线回调 */
6120
6335
  async _handleTransportDisconnect(error, closeCode, initialOptions) {
6336
+ if (this._udpStandby?.handlesDisconnect())
6337
+ return;
6121
6338
  if (this._closing || this._disconnecting || this.state === ConnectionState.CLOSED)
6122
6339
  return;
6123
6340
  // 已在重连中则跳过,避免心跳超时和 transport 断线回调重复触发
@@ -6198,6 +6415,50 @@ export class AUNClient {
6198
6415
  }
6199
6416
  this._startReconnect(serverInitiated, initialOptions, reconnectAbort);
6200
6417
  }
6418
+ _activateUdpStandby() {
6419
+ if (this._negotiatedCapabilities.udp_standby_v1 !== true || this._udpStandbyPolicy?.enabled !== true) {
6420
+ this._udpStandby?.deactivate();
6421
+ this._transport.setStandbyGate(null);
6422
+ return;
6423
+ }
6424
+ this._udpStandby ??= new UdpStandbyController({
6425
+ call: (method, params, timeout) => this._transport.call(method, params, timeout),
6426
+ canPrepare: async (timeout) => {
6427
+ if (!this._aid)
6428
+ return true;
6429
+ try {
6430
+ if (!this._aidStore) {
6431
+ const record = this._agentMdManager?._loadRecord?.(this._aid, true) ?? this._agentMdManager?.loadRecord(this._aid);
6432
+ const content = String(record?.local_draft_content ?? '') || String(record?.content ?? '');
6433
+ return !content || validateAgentMdReadDocument(content).frontmatter.type !== 'human';
6434
+ }
6435
+ const result = await this._aidStore.resolve(this._aid, { timeout });
6436
+ return !result.ok || result.data?.agent_md?.frontmatter.type !== 'human';
6437
+ }
6438
+ catch {
6439
+ return true;
6440
+ }
6441
+ },
6442
+ canStandby: () => this._state === 'ready' && !this._closing && !this._disconnecting && !this._reconnectActive
6443
+ && this._sessionOptions?.background_sync !== false
6444
+ && this._groupIdentityOperations.size === 0
6445
+ && this._transport.isConnected() && this._transport.rpcBudget.counts.total === 0
6446
+ && this._transport.foregroundWaiting === 0 && this._transport.rpcBudget.backgroundWaiting === 0
6447
+ && this._dispatcher.isIdle && !this._delivery?.hasAnyPendingPull()
6448
+ && (this._syncChannelCoordinator?.isIdle ?? true),
6449
+ prepareParams: () => ({ connection_id: this._gatewayConnectionId }),
6450
+ detach: () => this._transport.close(),
6451
+ reconnect: async () => {
6452
+ if (this._closing || this._disconnecting)
6453
+ throw new ConnectionError('client closed');
6454
+ this._state = 'standby';
6455
+ await this._lifecycle.connect(this._udpStandbyConnectOptions);
6456
+ },
6457
+ });
6458
+ this._udpStandby.activate(this._udpStandbyPolicy);
6459
+ const standby = this._udpStandby;
6460
+ this._transport.setStandbyGate((method, deadline, signal) => standby.acquire(method, deadline, signal), () => standby.touch(), params => standby.authenticationParams(params));
6461
+ }
6201
6462
  /** 启动重连循环(默认无限重试 + 当前基数抖动,仅在不可重试错误、close() 或 max_attempts 耗尽时终止) */
6202
6463
  _startReconnect(serverInitiated = false, initialOptions, existingAbort) {
6203
6464
  if (existingAbort) {