@okxweb3/a2a-node 0.2.17 → 0.2.18

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 (3) hide show
  1. package/dist/cli.js +597 -180
  2. package/dist/index.js +587 -170
  3. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -22179,7 +22179,7 @@ var require_retry_promise = __commonJS({
22179
22179
  return true;
22180
22180
  }
22181
22181
  };
22182
- function wait3(ms) {
22182
+ function wait2(ms) {
22183
22183
  return __awaiter(this, void 0, void 0, function() {
22184
22184
  return __generator(this, function(_a) {
22185
22185
  return [2, new Promise(function(resolve11) {
@@ -22188,7 +22188,7 @@ var require_retry_promise = __commonJS({
22188
22188
  });
22189
22189
  });
22190
22190
  }
22191
- exports2.wait = wait3;
22191
+ exports2.wait = wait2;
22192
22192
  function retry2(f, config) {
22193
22193
  return __awaiter(this, void 0, void 0, function() {
22194
22194
  var effectiveConfig;
@@ -22278,7 +22278,7 @@ var require_retry_promise = __commonJS({
22278
22278
  return [3, 5];
22279
22279
  case 5:
22280
22280
  millisToWait = delay(i + 1, config.delay);
22281
- return [4, wait3(millisToWait > config.maxBackOff ? config.maxBackOff : millisToWait)];
22281
+ return [4, wait2(millisToWait > config.maxBackOff ? config.maxBackOff : millisToWait)];
22282
22282
  case 6:
22283
22283
  _a.sent();
22284
22284
  if (done()) {
@@ -28761,7 +28761,7 @@ var init_withResolvers = __esm({
28761
28761
  });
28762
28762
 
28763
28763
  // ../../node_modules/viem/_esm/utils/promise/createBatchScheduler.js
28764
- function createBatchScheduler({ fn, id, shouldSplitBatch, wait: wait3 = 0, sort }) {
28764
+ function createBatchScheduler({ fn, id, shouldSplitBatch, wait: wait2 = 0, sort }) {
28765
28765
  const exec2 = async () => {
28766
28766
  const scheduler = getScheduler();
28767
28767
  flush2();
@@ -28799,7 +28799,7 @@ function createBatchScheduler({ fn, id, shouldSplitBatch, wait: wait3 = 0, sort
28799
28799
  return promise;
28800
28800
  }
28801
28801
  setScheduler({ args, resolve: resolve11, reject });
28802
- setTimeout(exec2, wait3);
28802
+ setTimeout(exec2, wait2);
28803
28803
  return promise;
28804
28804
  }
28805
28805
  };
@@ -32465,7 +32465,7 @@ var init_sentry_config = __esm({
32465
32465
  environment = process.env.SENTRY_ENV === "dev" ? "dev" : "prod";
32466
32466
  SENTRY_CONFIG = {
32467
32467
  projectName: "okx/openclaw-okx-a2a-extension",
32468
- release: "0.2.17",
32468
+ release: "0.2.18",
32469
32469
  environment,
32470
32470
  runtimeContainer: normalizeRuntimeContainer(process.env.OKX_A2A_RUNTIME_CONTAINER)
32471
32471
  };
@@ -33421,7 +33421,7 @@ async function getCurrentNodeCliVersion() {
33421
33421
  return await getGlobalNpmPackageVersion(UPDATE_PACKAGES.node) ?? getBundledNodeCliVersion();
33422
33422
  }
33423
33423
  function getBundledNodeCliVersion() {
33424
- return true ? "0.2.17" : null;
33424
+ return true ? "0.2.18" : null;
33425
33425
  }
33426
33426
  function readConfiguredAiProvider() {
33427
33427
  const explicit = process.env.OKX_A2A_AI_PROVIDER || process.env.OKX_AGENT_TASK_AI_CLI;
@@ -33631,7 +33631,7 @@ async function updateHermes(release, options) {
33631
33631
  }
33632
33632
  }
33633
33633
  async function installGatewayPluginForDoctor(target) {
33634
- const release = isPrereleaseVersion("0.2.17") ? "beta" : "latest";
33634
+ const release = isPrereleaseVersion("0.2.18") ? "beta" : "latest";
33635
33635
  const insideTargetGateway = detectGatewayInvocation() === target;
33636
33636
  const options = {
33637
33637
  restart: !insideTargetGateway,
@@ -49043,7 +49043,6 @@ function createAsyncStreamProxy(stream) {
49043
49043
  }
49044
49044
  });
49045
49045
  }
49046
- var wait = (ms) => new Promise((resolve11) => setTimeout(resolve11, ms));
49047
49046
  var DEFAULT_RETRY_DELAY = 6e4;
49048
49047
  var DEFAULT_RETRY_ATTEMPTS = 10;
49049
49048
  var createStream = async (streamFunction, streamValueMutator, options) => {
@@ -49052,82 +49051,161 @@ var createStream = async (streamFunction, streamValueMutator, options) => {
49052
49051
  throw new StreamInvalidRetryAttemptsError();
49053
49052
  }
49054
49053
  const asyncStream = new AsyncStream();
49054
+ let stopped = false;
49055
+ let current;
49056
+ let recovering = false;
49057
+ let cancelDelay;
49058
+ const isStopped = () => stopped;
49059
+ const isCurrent = (native) => !stopped && current === native;
49060
+ asyncStream.onDone = () => {
49061
+ if (stopped) {
49062
+ return;
49063
+ }
49064
+ stopped = true;
49065
+ cancelDelay?.();
49066
+ const native = current;
49067
+ current = void 0;
49068
+ native?.closer?.end();
49069
+ };
49070
+ const delay = (ms) => new Promise((resolve11) => {
49071
+ const timer = setTimeout(() => {
49072
+ cancelDelay = void 0;
49073
+ resolve11();
49074
+ }, ms);
49075
+ cancelDelay = () => {
49076
+ clearTimeout(timer);
49077
+ cancelDelay = void 0;
49078
+ resolve11();
49079
+ };
49080
+ });
49081
+ const reportError = (error) => {
49082
+ if (!stopped) {
49083
+ onError?.(error);
49084
+ }
49085
+ };
49086
+ const deliver = (value) => {
49087
+ if (stopped || value === void 0) {
49088
+ return;
49089
+ }
49090
+ asyncStream.push(value);
49091
+ onValue?.(value);
49092
+ };
49055
49093
  const streamCallback = (error, value) => {
49094
+ if (stopped) {
49095
+ return;
49096
+ }
49056
49097
  if (error) {
49057
49098
  onError?.(error);
49058
49099
  return;
49059
49100
  }
49060
- if (value !== void 0) {
49061
- try {
49062
- if (streamValueMutator) {
49063
- const mutatedValue = streamValueMutator(value);
49064
- if ((0, import_types.isPromise)(mutatedValue)) {
49065
- void mutatedValue.then((mutatedValue2) => {
49066
- if (mutatedValue2 !== void 0) {
49067
- asyncStream.push(mutatedValue2);
49068
- onValue?.(mutatedValue2);
49069
- }
49070
- }).catch((error2) => {
49071
- onError?.(error2);
49072
- });
49073
- } else {
49074
- if (mutatedValue !== void 0) {
49075
- asyncStream.push(mutatedValue);
49076
- onValue?.(mutatedValue);
49077
- }
49078
- }
49101
+ if (value === void 0) {
49102
+ return;
49103
+ }
49104
+ try {
49105
+ if (streamValueMutator) {
49106
+ const mutatedValue = streamValueMutator(value);
49107
+ if ((0, import_types.isPromise)(mutatedValue)) {
49108
+ void mutatedValue.then(deliver).catch(reportError);
49079
49109
  } else {
49080
- asyncStream.push(value);
49081
- onValue?.(value);
49110
+ deliver(mutatedValue);
49082
49111
  }
49083
- } catch (error2) {
49084
- onError?.(error2);
49112
+ } else {
49113
+ deliver(value);
49085
49114
  }
49115
+ } catch (error2) {
49116
+ reportError(error2);
49086
49117
  }
49087
49118
  };
49088
- const retry2 = async (retries = retryAttempts) => {
49089
- if (retries === 0) {
49090
- void asyncStream.end();
49091
- onError?.(new StreamFailedError(retryAttempts));
49092
- return;
49119
+ const fail = (error) => {
49120
+ void asyncStream.end();
49121
+ onError?.(error);
49122
+ };
49123
+ const open2 = async () => {
49124
+ if (stopped) {
49125
+ return false;
49126
+ }
49127
+ const native = { ready: false, closed: false };
49128
+ current = native;
49129
+ const closer = await streamFunction(streamCallback, () => {
49130
+ onNativeClose(native);
49131
+ });
49132
+ native.closer = closer;
49133
+ if (!isCurrent(native)) {
49134
+ closer.end();
49135
+ return false;
49136
+ }
49137
+ await closer.waitForReady();
49138
+ if (!isCurrent(native)) {
49139
+ closer.end();
49140
+ return false;
49141
+ }
49142
+ if (native.closed || closer.isClosed()) {
49143
+ closer.end();
49144
+ return false;
49093
49145
  }
49094
- await wait(retryDelay);
49095
- onRetry?.(retryAttempts - retries + 1, retryAttempts);
49146
+ native.ready = true;
49147
+ return true;
49148
+ };
49149
+ const restart = async () => {
49096
49150
  try {
49097
- const streamCloser = await streamFunction(streamCallback, () => {
49098
- onFail?.();
49099
- void retry2();
49100
- });
49101
- await streamCloser.waitForReady();
49102
- asyncStream.onDone = () => {
49103
- streamCloser.end();
49104
- };
49105
- onRestart?.();
49106
- } catch (error) {
49107
- onError?.(error);
49108
- void retry2(retries - 1);
49151
+ for (let attempt = 1; attempt <= retryAttempts; attempt++) {
49152
+ await delay(retryDelay);
49153
+ if (isStopped()) {
49154
+ return;
49155
+ }
49156
+ onRetry?.(attempt, retryAttempts);
49157
+ let restarted = false;
49158
+ try {
49159
+ restarted = await open2();
49160
+ } catch (error) {
49161
+ reportError(error);
49162
+ }
49163
+ if (isStopped()) {
49164
+ return;
49165
+ }
49166
+ if (restarted) {
49167
+ try {
49168
+ onRestart?.();
49169
+ } catch (error) {
49170
+ reportError(error);
49171
+ }
49172
+ return;
49173
+ }
49174
+ }
49175
+ fail(new StreamFailedError(retryAttempts));
49176
+ } finally {
49177
+ recovering = false;
49109
49178
  }
49110
49179
  };
49111
- const startRetry = () => {
49112
- if (retryOnFail) {
49113
- void retry2();
49114
- } else {
49115
- void asyncStream.end();
49116
- onError?.(new StreamFailedError(0));
49180
+ const recover = () => {
49181
+ if (stopped || recovering) {
49182
+ return;
49117
49183
  }
49184
+ if (!retryOnFail) {
49185
+ fail(new StreamFailedError(0));
49186
+ return;
49187
+ }
49188
+ recovering = true;
49189
+ void restart();
49118
49190
  };
49191
+ function onNativeClose(native) {
49192
+ if (!isCurrent(native) || native.closed) {
49193
+ return;
49194
+ }
49195
+ native.closed = true;
49196
+ onFail?.();
49197
+ if (native.ready) {
49198
+ recover();
49199
+ }
49200
+ }
49201
+ let started = false;
49119
49202
  try {
49120
- const streamCloser = await streamFunction(streamCallback, () => {
49121
- onFail?.();
49122
- startRetry();
49123
- });
49124
- await streamCloser.waitForReady();
49125
- asyncStream.onDone = () => {
49126
- streamCloser.end();
49127
- };
49203
+ started = await open2();
49128
49204
  } catch (error) {
49129
- onError?.(error);
49130
- startRetry();
49205
+ reportError(error);
49206
+ }
49207
+ if (!started) {
49208
+ recover();
49131
49209
  }
49132
49210
  return createAsyncStreamProxy(asyncStream);
49133
49211
  };
@@ -52371,7 +52449,7 @@ function observe(observerId, callbacks, fn) {
52371
52449
  }
52372
52450
 
52373
52451
  // ../../node_modules/viem/_esm/utils/wait.js
52374
- async function wait2(time) {
52452
+ async function wait(time) {
52375
52453
  return new Promise((res) => setTimeout(res, time));
52376
52454
  }
52377
52455
 
@@ -52384,12 +52462,12 @@ function poll(fn, { emitOnBegin, initialWaitTime, interval }) {
52384
52462
  if (emitOnBegin)
52385
52463
  data = await fn({ unpoll: unwatch });
52386
52464
  const initialWait = await initialWaitTime?.(data) ?? interval;
52387
- await wait2(initialWait);
52465
+ await wait(initialWait);
52388
52466
  const poll2 = async () => {
52389
52467
  if (!active)
52390
52468
  return;
52391
52469
  await fn({ unpoll: unwatch });
52392
- await wait2(interval);
52470
+ await wait(interval);
52393
52471
  poll2();
52394
52472
  };
52395
52473
  poll2();
@@ -52712,7 +52790,7 @@ function withRetry(fn, { delay: delay_ = 100, retryCount = 2, shouldRetry: shoul
52712
52790
  const retry2 = async ({ error }) => {
52713
52791
  const delay = typeof delay_ === "function" ? delay_({ count, error }) : delay_;
52714
52792
  if (delay)
52715
- await wait2(delay);
52793
+ await wait(delay);
52716
52794
  attemptRetry({ count: count + 1 });
52717
52795
  };
52718
52796
  try {
@@ -54875,7 +54953,7 @@ init_createBatchScheduler();
54875
54953
  function http(url, config = {}) {
54876
54954
  const { batch, fetchFn, fetchOptions, key = "http", methods, name = "HTTP JSON-RPC", onFetchRequest, onFetchResponse, retryDelay, raw } = config;
54877
54955
  return ({ chain, retryCount: retryCount_, timeout: timeout_ }) => {
54878
- const { batchSize = 1e3, wait: wait3 = 0 } = typeof batch === "object" ? batch : {};
54956
+ const { batchSize = 1e3, wait: wait2 = 0 } = typeof batch === "object" ? batch : {};
54879
54957
  const retryCount = config.retryCount ?? retryCount_;
54880
54958
  const timeout = timeout_ ?? config.timeout ?? 1e4;
54881
54959
  const url_ = url || chain?.rpcUrls.default.http[0];
@@ -54896,7 +54974,7 @@ function http(url, config = {}) {
54896
54974
  const body = { method, params };
54897
54975
  const { schedule } = createBatchScheduler({
54898
54976
  id: url_,
54899
- wait: wait3,
54977
+ wait: wait2,
54900
54978
  shouldSplitBatch(requests) {
54901
54979
  return requests.length > batchSize;
54902
54980
  },
@@ -55113,7 +55191,7 @@ var sepolia = /* @__PURE__ */ defineChain({
55113
55191
 
55114
55192
  // ../../node_modules/@xmtp/agent-sdk/dist/index.js
55115
55193
  var import_node_crypto7 = require("node:crypto");
55116
- var version2 = "2.3.0";
55194
+ var version2 = "2.3.1";
55117
55195
  var fromSelf = (message, client) => {
55118
55196
  return message.senderInboxId === client.inboxId;
55119
55197
  };
@@ -57162,6 +57240,9 @@ function replayScanBackoffWithJitterMs(consecutiveFailures, randomValue = Math.r
57162
57240
  const jitteredMs = Math.round(baseMs * (0.9 + boundedRandom * 0.2));
57163
57241
  return Math.min(REPLAY_SCAN_MAX_BACKOFF_MS, jitteredMs);
57164
57242
  }
57243
+ function isListenHealthy(outcome) {
57244
+ return outcome === "started" || outcome === "retrying";
57245
+ }
57165
57246
  function isOfflineReplayForTrigger(trigger) {
57166
57247
  return trigger === "startup";
57167
57248
  }
@@ -57171,6 +57252,18 @@ function replaySingleFlightDiscriminator(trigger, clientGeneration) {
57171
57252
  isOfflineReplayForTrigger(trigger)
57172
57253
  );
57173
57254
  }
57255
+ var XmtpClientCreateBlockedError = class extends Error {
57256
+ reason;
57257
+ address;
57258
+ agentId;
57259
+ constructor(reason, address, agentId) {
57260
+ super(`xmtp client create blocked (${reason})`);
57261
+ this.name = "XmtpClientCreateBlockedError";
57262
+ this.reason = reason;
57263
+ this.address = address;
57264
+ this.agentId = agentId;
57265
+ }
57266
+ };
57174
57267
  var XmtpService = class _XmtpService {
57175
57268
  static instance = null;
57176
57269
  env = process.env.XMTP_ENV ?? "production";
@@ -57208,6 +57301,25 @@ var XmtpService = class _XmtpService {
57208
57301
  streamRecoverySetTimeout = (callback, delayMs) => setTimeout(callback, delayMs);
57209
57302
  streamRecoveryClearTimeout = (timer) => clearTimeout(timer);
57210
57303
  streamStartSetTimeout = (callback, delayMs) => setTimeout(callback, delayMs);
57304
+ xmtpAgentFactory = createAgentWithInstallationPrune;
57305
+ // Old clients whose close() was not confirmed. Keyed by lower-cased address.
57306
+ // The reference is kept only for controlled cleanup; it is never served.
57307
+ pendingReleaseByAddress = /* @__PURE__ */ new Map();
57308
+ // Addresses whose pending release was confirmed late (after the caller gave
57309
+ // up waiting) and still need a replacement client on the next recycle.
57310
+ releaseConfirmedNeedsCreate = /* @__PURE__ */ new Set();
57311
+ // Lower-cased addresses owned by a create. The slot is held from before the
57312
+ // SDK factory runs until the new client is registered in `clients` (or the
57313
+ // create failed), so no flow can observe "no lock and no client".
57314
+ clientCreateInFlight = /* @__PURE__ */ new Set();
57315
+ // Clients whose close() was confirmed; a repeated release is a no-op.
57316
+ releasedClients = /* @__PURE__ */ new WeakSet();
57317
+ // One listener setup per client, shared by concurrent init/refresh callers.
57318
+ listenTaskByClient = /* @__PURE__ */ new WeakMap();
57319
+ inboundReplayGateOwners = /* @__PURE__ */ new WeakMap();
57320
+ releaseWaitTimeoutMs = 3e4;
57321
+ releaseWaitSetTimeout = (callback, delayMs) => setTimeout(callback, delayMs);
57322
+ releaseWaitClearTimeout = (timer) => clearTimeout(timer);
57211
57323
  nextClientGeneration = 1;
57212
57324
  clientGenerationByClient = /* @__PURE__ */ new WeakMap();
57213
57325
  resolveStartupReplayCompletion;
@@ -57356,6 +57468,56 @@ var XmtpService = class _XmtpService {
57356
57468
  return { ok: remaining.length === 0, failures: remaining };
57357
57469
  }
57358
57470
  async createClientForAgent(agentInfo, options = {}) {
57471
+ const address = agentInfo.communicationAddress;
57472
+ const addressKey = address.toLowerCase();
57473
+ let blockedReason;
57474
+ if (this.pendingReleaseByAddress.has(addressKey)) {
57475
+ blockedReason = "release_pending";
57476
+ } else if (this.clientCreateInFlight.has(addressKey)) {
57477
+ blockedReason = "create_in_flight";
57478
+ } else if (this.getClientByAddress(address)) {
57479
+ blockedReason = "client_active";
57480
+ }
57481
+ if (blockedReason) {
57482
+ throw new XmtpClientCreateBlockedError(
57483
+ blockedReason,
57484
+ address,
57485
+ agentInfo.agentId
57486
+ );
57487
+ }
57488
+ this.clientCreateInFlight.add(addressKey);
57489
+ try {
57490
+ const created = await this.createClientForAgentGuarded(agentInfo, options);
57491
+ await this.registerCreatedClient(created, options.logPrefix ?? "[xmtp-sdk]");
57492
+ return created;
57493
+ } finally {
57494
+ this.clientCreateInFlight.delete(addressKey);
57495
+ }
57496
+ }
57497
+ // Ownership hand-off, still inside the create slot: the new client becomes
57498
+ // visible in `clients` before the slot is released. Never overwrites an
57499
+ // existing registration; a conflicting new object is released, not dropped.
57500
+ async registerCreatedClient(created, phase) {
57501
+ const { address, agent, agentInfo } = created;
57502
+ const addressKey = address.toLowerCase();
57503
+ const existing = this.getClientByAddress(address);
57504
+ if (existing && existing !== agent || this.pendingReleaseByAddress.has(addressKey)) {
57505
+ const reason = existing ? "client_active" : "release_pending";
57506
+ logWithTimestamp(
57507
+ `[xmtp-sdk] create registration conflict (${reason}): ${address} (agentId=${agentInfo.agentId}), releasing the new client`
57508
+ );
57509
+ await this.releaseClient(
57510
+ address,
57511
+ agent,
57512
+ agentInfo.agentId,
57513
+ `${phase}/register-conflict`
57514
+ );
57515
+ throw new XmtpClientCreateBlockedError(reason, address, agentInfo.agentId);
57516
+ }
57517
+ this.stoppedAddresses.delete(addressKey);
57518
+ this.clients.set(address, agent);
57519
+ }
57520
+ async createClientForAgentGuarded(agentInfo, options = {}) {
57359
57521
  const address = agentInfo.communicationAddress;
57360
57522
  const phase = options.logPrefix ?? "[xmtp-sdk]";
57361
57523
  const cooldown = getXmtpCreationCooldown();
@@ -57384,7 +57546,7 @@ var XmtpService = class _XmtpService {
57384
57546
  try {
57385
57547
  const signer = this.tools.createSigner(agentInfo);
57386
57548
  const dbPath = (inboxId2) => `${this.dataDir}/${inboxId2}-${this.env}.db3`;
57387
- const agent = await createAgentWithInstallationPrune(signer, {
57549
+ const agent = await this.xmtpAgentFactory(signer, {
57388
57550
  dbPath,
57389
57551
  env: this.env
57390
57552
  }, {
@@ -57393,6 +57555,7 @@ var XmtpService = class _XmtpService {
57393
57555
  logPrefix: options.logPrefix
57394
57556
  });
57395
57557
  cooldown.recordCreateSuccess(decision);
57558
+ this.releaseConfirmedNeedsCreate.delete(address.toLowerCase());
57396
57559
  if (decision.mode === "probe") {
57397
57560
  logger.info(LogEvent.XMTP_RATE_LIMIT_RECOVERED, {
57398
57561
  onchainosAgentId: agentInfo.agentId,
@@ -57430,6 +57593,128 @@ var XmtpService = class _XmtpService {
57430
57593
  throw err;
57431
57594
  }
57432
57595
  }
57596
+ logReleaseNotConfirmed(address, agentId, phase, reason, err) {
57597
+ logger.error(
57598
+ LogEvent.XMTP_CLIENT_CLOSE_FAILED,
57599
+ err instanceof Error ? err : new Error(`xmtp client close not confirmed (${reason})`),
57600
+ { walletAddress: address, onchainosAgentId: agentId, phase, reason }
57601
+ );
57602
+ }
57603
+ // Single release entry (recycle / refresh remove / failed replacement).
57604
+ // Confirmation is ONLY a successful client.close(); a timeout, a missing
57605
+ // close() or a close failure keeps the old reference in
57606
+ // pendingReleaseByAddress and blocks new creates for that address.
57607
+ async releaseClient(address, client, agentId, phase) {
57608
+ const key = address.toLowerCase();
57609
+ if (this.releasedClients.has(client)) {
57610
+ this.unpublishClient(address, client, phase);
57611
+ return true;
57612
+ }
57613
+ const existing = this.pendingReleaseByAddress.get(key);
57614
+ if (existing?.inFlight) {
57615
+ this.unpublishClient(address, client, phase);
57616
+ return false;
57617
+ }
57618
+ const entry = existing ?? { client, agentId, phase };
57619
+ entry.client = client;
57620
+ entry.agentId = agentId;
57621
+ entry.phase = phase;
57622
+ this.pendingReleaseByAddress.set(key, entry);
57623
+ this.unpublishClient(address, client, phase);
57624
+ let timedOut = false;
57625
+ const task = stopAndCloseXmtpClient(client, {
57626
+ address,
57627
+ agentId,
57628
+ phase,
57629
+ onCloseError: (err) => this.logReleaseNotConfirmed(address, agentId, phase, "close_threw", err)
57630
+ }).then((result) => {
57631
+ if (!result.stopped) {
57632
+ logWithTimestamp(
57633
+ `[xmtp-sdk] release: stop failed but close proceeded: ${address} (agentId=${agentId}) phase=${phase}`
57634
+ );
57635
+ }
57636
+ entry.inFlight = void 0;
57637
+ if (result.closed) {
57638
+ this.releasedClients.add(client);
57639
+ }
57640
+ if (result.closed && this.pendingReleaseByAddress.get(key) === entry) {
57641
+ this.pendingReleaseByAddress.delete(key);
57642
+ if (timedOut) {
57643
+ this.releaseConfirmedNeedsCreate.add(key);
57644
+ }
57645
+ }
57646
+ return result;
57647
+ });
57648
+ entry.inFlight = task.then((result) => result.closed);
57649
+ const timeoutHandle = {};
57650
+ const timeoutPromise = new Promise((resolve11) => {
57651
+ timeoutHandle.timer = this.releaseWaitSetTimeout(
57652
+ () => resolve11(null),
57653
+ this.releaseWaitTimeoutMs
57654
+ );
57655
+ });
57656
+ const outcome = await Promise.race([task, timeoutPromise]);
57657
+ if (timeoutHandle.timer !== void 0) {
57658
+ this.releaseWaitClearTimeout(timeoutHandle.timer);
57659
+ }
57660
+ if (outcome === null) {
57661
+ timedOut = true;
57662
+ this.logReleaseNotConfirmed(address, agentId, phase, "close_timeout");
57663
+ return false;
57664
+ }
57665
+ if (outcome.closed) {
57666
+ if (this.pendingReleaseByAddress.get(key) === entry) {
57667
+ this.pendingReleaseByAddress.delete(key);
57668
+ }
57669
+ return true;
57670
+ }
57671
+ if (!outcome.closeFailed) {
57672
+ this.logReleaseNotConfirmed(address, agentId, phase, "close_unavailable");
57673
+ }
57674
+ return false;
57675
+ }
57676
+ // Stops serving `client` to new operations. Every removal is identity
57677
+ // checked, so a late caller can never remove a newer instance (or its replay
57678
+ // gate / recovery / retry state) registered for the same address.
57679
+ unpublishClient(address, client, phase) {
57680
+ const key = address.toLowerCase();
57681
+ let removed = false;
57682
+ for (const [registeredAddress, registered] of this.clients) {
57683
+ if (registered === client && registeredAddress.toLowerCase() === key) {
57684
+ this.clients.delete(registeredAddress);
57685
+ removed = true;
57686
+ }
57687
+ }
57688
+ const gate = this.inboundReplayGates.get(key);
57689
+ if (gate) {
57690
+ const owner = this.inboundReplayGateOwners.get(gate);
57691
+ if (owner === client || owner === void 0 && removed) {
57692
+ this.inboundReplayGates.delete(key);
57693
+ }
57694
+ }
57695
+ this.cancelStreamRecoveryForAddress(address, client);
57696
+ if (removed) {
57697
+ this.replayScanFailuresByAddress.delete(key);
57698
+ this.clearReplayScanRetry(
57699
+ address,
57700
+ phase.startsWith("recycle") ? "client_recycled" : "client_removed"
57701
+ );
57702
+ }
57703
+ if (!this.getClientByAddress(address)) {
57704
+ this.stoppedAddresses.add(key);
57705
+ }
57706
+ }
57707
+ async settlePendingRelease(address) {
57708
+ const entry = this.pendingReleaseByAddress.get(address.toLowerCase());
57709
+ if (!entry) {
57710
+ return true;
57711
+ }
57712
+ if (entry.inFlight) {
57713
+ return false;
57714
+ }
57715
+ const retryPhase = entry.phase.endsWith("/retry") ? entry.phase : `${entry.phase}/retry`;
57716
+ return this.releaseClient(address, entry.client, entry.agentId, retryPhase);
57717
+ }
57433
57718
  async recordInstallationHealth(params) {
57434
57719
  const { address, agent, agentInfo, phase, attempt } = params;
57435
57720
  try {
@@ -57550,8 +57835,6 @@ var XmtpService = class _XmtpService {
57550
57835
  const r = results[i];
57551
57836
  if (r.status === "fulfilled") {
57552
57837
  const { address, agent, agentInfo } = r.value;
57553
- this.stoppedAddresses.delete(address.toLowerCase());
57554
- this.clients.set(address, agent);
57555
57838
  logWithTimestamp(
57556
57839
  `[xmtp-sdk] identity initialized: ${address} (agentId=${agentInfo.agentId})`
57557
57840
  );
@@ -57577,6 +57860,10 @@ var XmtpService = class _XmtpService {
57577
57860
  phase: "init",
57578
57861
  attempt: "1"
57579
57862
  });
57863
+ } else if (r.reason instanceof XmtpClientCreateBlockedError) {
57864
+ logWithTimestamp(
57865
+ `[xmtp-sdk] identity init: create blocked (${r.reason.reason}): ${agents[i].communicationAddress} (agentId=${agents[i].agentId})`
57866
+ );
57580
57867
  } else {
57581
57868
  failedAgents.push(agents[i]);
57582
57869
  if (r.reason instanceof XmtpCreationCooldownSkipError) {
@@ -57613,8 +57900,6 @@ var XmtpService = class _XmtpService {
57613
57900
  const r = retryResults[i];
57614
57901
  if (r.status === "fulfilled") {
57615
57902
  const { address, agent, agentInfo } = r.value;
57616
- this.stoppedAddresses.delete(address.toLowerCase());
57617
- this.clients.set(address, agent);
57618
57903
  logWithTimestamp(
57619
57904
  `[xmtp-sdk] retry succeeded: ${address} (agentId=${agentInfo.agentId})`
57620
57905
  );
@@ -57640,6 +57925,11 @@ var XmtpService = class _XmtpService {
57640
57925
  phase: "init",
57641
57926
  attempt: "2"
57642
57927
  });
57928
+ } else if (r.reason instanceof XmtpClientCreateBlockedError) {
57929
+ const info = failedAgents[i];
57930
+ logWithTimestamp(
57931
+ `[xmtp-sdk] retry create blocked (${r.reason.reason}): ${info.communicationAddress} (agentId=${info.agentId})`
57932
+ );
57643
57933
  } else if (r.reason instanceof XmtpCreationCooldownSkipError) {
57644
57934
  const info = failedAgents[i];
57645
57935
  logWithTimestamp(
@@ -57722,36 +58012,38 @@ var XmtpService = class _XmtpService {
57722
58012
  throw err;
57723
58013
  }
57724
58014
  }
58015
+ for (const pendingKey of [...this.pendingReleaseByAddress.keys()]) {
58016
+ const settled = await this.settlePendingRelease(pendingKey);
58017
+ logWithTimestamp(
58018
+ `[xmtp-sdk] refresh: pending release ${pendingKey} ${settled ? "confirmed" : "still pending"}`
58019
+ );
58020
+ }
57725
58021
  const oldAddresses = new Set(
57726
- this.allAgents.map((a) => a.communicationAddress)
58022
+ this.allAgents.map((a) => a.communicationAddress.toLowerCase())
58023
+ );
58024
+ const newAddresses = new Set(
58025
+ newAgents.map((a) => a.communicationAddress.toLowerCase())
57727
58026
  );
57728
- const newAddresses = new Set(newAgents.map((a) => a.communicationAddress));
57729
58027
  const added = newAgents.filter(
57730
- (a) => !oldAddresses.has(a.communicationAddress) || !this.clients.has(a.communicationAddress)
58028
+ (a) => !oldAddresses.has(a.communicationAddress.toLowerCase()) || !this.getClientByAddress(a.communicationAddress)
57731
58029
  );
57732
58030
  const removed = this.allAgents.filter(
57733
- (a) => !newAddresses.has(a.communicationAddress)
58031
+ (a) => !newAddresses.has(a.communicationAddress.toLowerCase())
57734
58032
  );
57735
58033
  this.allAgents = newAgents;
57736
58034
  for (const agentInfo of removed) {
57737
58035
  const address = agentInfo.communicationAddress;
57738
- const client = this.clients.get(address);
58036
+ const client = this.getClientByAddress(address);
57739
58037
  if (client) {
57740
58038
  try {
57741
- this.cancelStreamRecoveryForAddress(address);
57742
- this.stoppedAddresses.add(address.toLowerCase());
57743
- await stopAndCloseXmtpClient(client, {
58039
+ const released = await this.releaseClient(
57744
58040
  address,
57745
- agentId: agentInfo.agentId,
57746
- phase: "refresh/remove",
57747
- onCloseError: (err) => logger.error(
57748
- LogEvent.XMTP_CLIENT_CLOSE_FAILED,
57749
- err instanceof Error ? err : new Error(String(err)),
57750
- { walletAddress: address, onchainosAgentId: agentInfo.agentId, phase: "refresh/remove" }
57751
- )
57752
- });
58041
+ client,
58042
+ agentInfo.agentId,
58043
+ "refresh/remove"
58044
+ );
57753
58045
  logWithTimestamp(
57754
- `[xmtp-sdk] refresh: stopped client: ${address} (agentId=${agentInfo.agentId})`
58046
+ `[xmtp-sdk] refresh: stopped client: ${address} (agentId=${agentInfo.agentId}) released=${released}`
57755
58047
  );
57756
58048
  } catch (err) {
57757
58049
  logWithTimestamp(`[xmtp-sdk] refresh: failed to stop client: ${address}`, err);
@@ -57761,10 +58053,6 @@ var XmtpService = class _XmtpService {
57761
58053
  phase: "stop-client"
57762
58054
  });
57763
58055
  }
57764
- this.clients.delete(address);
57765
- this.inboundReplayGates.delete(address.toLowerCase());
57766
- this.replayScanFailuresByAddress.delete(address.toLowerCase());
57767
- this.clearReplayScanRetry(address, "client_removed");
57768
58056
  logger.info(LogEvent.AGENT_CLIENT_REMOVED, {
57769
58057
  ...agentExtras({
57770
58058
  walletAddress: address,
@@ -57806,8 +58094,6 @@ var XmtpService = class _XmtpService {
57806
58094
  const r = addResults[i];
57807
58095
  if (r.status === "fulfilled") {
57808
58096
  const { address, agentInfo } = r.value;
57809
- this.stoppedAddresses.delete(address.toLowerCase());
57810
- this.clients.set(address, r.value.agent);
57811
58097
  logWithTimestamp(
57812
58098
  `[xmtp-sdk] refresh: identity initialized: ${address} (agentId=${agentInfo.agentId})`
57813
58099
  );
@@ -57834,6 +58120,10 @@ var XmtpService = class _XmtpService {
57834
58120
  phase: "refresh",
57835
58121
  attempt: "1"
57836
58122
  });
58123
+ } else if (r.reason instanceof XmtpClientCreateBlockedError) {
58124
+ logWithTimestamp(
58125
+ `[xmtp-sdk] refresh: create blocked (${r.reason.reason}): ${added[i].communicationAddress} (agentId=${added[i].agentId})`
58126
+ );
57837
58127
  } else {
57838
58128
  failedAgents.push(added[i]);
57839
58129
  if (r.reason instanceof XmtpCreationCooldownSkipError) {
@@ -57873,8 +58163,6 @@ var XmtpService = class _XmtpService {
57873
58163
  const r = retryResults[i];
57874
58164
  if (r.status === "fulfilled") {
57875
58165
  const { address, agentInfo } = r.value;
57876
- this.stoppedAddresses.delete(address.toLowerCase());
57877
- this.clients.set(address, r.value.agent);
57878
58166
  logWithTimestamp(
57879
58167
  `[xmtp-sdk] refresh: retry succeeded: ${address} (agentId=${agentInfo.agentId})`
57880
58168
  );
@@ -57901,6 +58189,11 @@ var XmtpService = class _XmtpService {
57901
58189
  phase: "refresh",
57902
58190
  attempt: "2"
57903
58191
  });
58192
+ } else if (r.reason instanceof XmtpClientCreateBlockedError) {
58193
+ const info = failedAgents[i];
58194
+ logWithTimestamp(
58195
+ `[xmtp-sdk] refresh: retry create blocked (${r.reason.reason}): ${info.communicationAddress} (agentId=${info.agentId})`
58196
+ );
57904
58197
  } else if (r.reason instanceof XmtpCreationCooldownSkipError) {
57905
58198
  const info = failedAgents[i];
57906
58199
  logWithTimestamp(
@@ -57926,7 +58219,8 @@ var XmtpService = class _XmtpService {
57926
58219
  }
57927
58220
  }
57928
58221
  await this.startListeningForAddresses(
57929
- added.map((a) => a.communicationAddress)
58222
+ added.map((a) => a.communicationAddress),
58223
+ "refresh/listen-failed"
57930
58224
  );
57931
58225
  for (const agentInfo of added) {
57932
58226
  const address = agentInfo.communicationAddress;
@@ -57991,30 +58285,42 @@ var XmtpService = class _XmtpService {
57991
58285
  }
57992
58286
  for (const agentInfo of this.allAgents) {
57993
58287
  const address = agentInfo.communicationAddress;
57994
- const oldClient = this.clients.get(address);
57995
- if (!oldClient) {
58288
+ const addressKey = address.toLowerCase();
58289
+ const oldClient = this.getClientByAddress(address);
58290
+ const hasPendingRelease = this.pendingReleaseByAddress.has(addressKey) || this.releaseConfirmedNeedsCreate.has(addressKey);
58291
+ if (!oldClient && !hasPendingRelease) {
57996
58292
  continue;
57997
58293
  }
57998
- const addressKey = address.toLowerCase();
57999
- try {
58000
- this.cancelStreamRecoveryForAddress(address);
58001
- this.stoppedAddresses.add(addressKey);
58002
- await stopAndCloseXmtpClient(oldClient, {
58003
- address,
58004
- agentId: agentInfo.agentId,
58005
- phase: "recycle",
58006
- onCloseError: (err) => logger.error(
58007
- LogEvent.XMTP_CLIENT_CLOSE_FAILED,
58008
- err instanceof Error ? err : new Error(String(err)),
58009
- { walletAddress: address, onchainosAgentId: agentInfo.agentId, phase: "recycle" }
58010
- )
58011
- });
58012
- this.clients.delete(address);
58013
- this.inboundReplayGates.delete(addressKey);
58014
- this.replayScanFailuresByAddress.delete(addressKey);
58015
- this.clearReplayScanRetry(address, "client_recycled");
58294
+ if (!oldClient) {
58295
+ let settled = false;
58296
+ try {
58297
+ settled = await this.settlePendingRelease(address);
58298
+ } catch (err) {
58299
+ logWithTimestamp(`[xmtp-sdk] recycle: pending release retry failed: ${address}`, err);
58300
+ }
58301
+ if (!settled) {
58302
+ failed++;
58303
+ continue;
58304
+ }
58305
+ } else {
58306
+ let released = false;
58307
+ try {
58308
+ released = await this.releaseClient(
58309
+ address,
58310
+ oldClient,
58311
+ agentInfo.agentId,
58312
+ "recycle"
58313
+ );
58314
+ } catch (err) {
58315
+ logWithTimestamp(`[xmtp-sdk] recycle: failed to stop client: ${address}`, err);
58316
+ logger.error(LogEvent.AGENT_REFRESH_FAILED, err instanceof Error ? err : new Error(String(err)), {
58317
+ walletAddress: address,
58318
+ onchainosAgentId: agentInfo.agentId,
58319
+ phase: "recycle/stop-client"
58320
+ });
58321
+ }
58016
58322
  logWithTimestamp(
58017
- `[xmtp-sdk] recycle: stopped client: ${address} (agentId=${agentInfo.agentId})`
58323
+ `[xmtp-sdk] recycle: stopped client: ${address} (agentId=${agentInfo.agentId}) released=${released}`
58018
58324
  );
58019
58325
  logger.info(LogEvent.AGENT_CLIENT_REMOVED, {
58020
58326
  ...agentExtras({
@@ -58024,23 +58330,18 @@ var XmtpService = class _XmtpService {
58024
58330
  }),
58025
58331
  phase: "recycle"
58026
58332
  });
58027
- } catch (err) {
58028
- failed++;
58029
- this.stoppedAddresses.delete(addressKey);
58030
- logWithTimestamp(`[xmtp-sdk] recycle: failed to stop client: ${address}`, err);
58031
- logger.error(LogEvent.AGENT_REFRESH_FAILED, err instanceof Error ? err : new Error(String(err)), {
58032
- walletAddress: address,
58033
- onchainosAgentId: agentInfo.agentId,
58034
- phase: "recycle/stop-client"
58035
- });
58036
- continue;
58333
+ if (!released) {
58334
+ failed++;
58335
+ continue;
58336
+ }
58037
58337
  }
58338
+ let newAgent;
58339
+ let newAgentReleased = false;
58038
58340
  try {
58039
58341
  const { agent } = await this.createClientForAgent(agentInfo, {
58040
58342
  logPrefix: "[xmtp-sdk:recycle]"
58041
58343
  });
58042
- this.stoppedAddresses.delete(addressKey);
58043
- this.clients.set(address, agent);
58344
+ newAgent = agent;
58044
58345
  logWithTimestamp(
58045
58346
  `[xmtp-sdk] recycle: identity initialized: ${address} (agentId=${agentInfo.agentId})`
58046
58347
  );
@@ -58064,7 +58365,13 @@ var XmtpService = class _XmtpService {
58064
58365
  agentInfo,
58065
58366
  phase: "recycle"
58066
58367
  });
58067
- await this.startListeningForAddresses([address]);
58368
+ const listen = (await this.startListeningForAddresses([address], "recycle/listen-failed")).get(address);
58369
+ if (listen?.client !== agent || !isListenHealthy(listen.outcome)) {
58370
+ newAgentReleased = listen?.client === agent && listen.outcome === "failed";
58371
+ throw new Error(
58372
+ `xmtp listener not started (${listen?.client === agent ? listen.outcome : "client_replaced"})`
58373
+ );
58374
+ }
58068
58375
  const replayGate = this.inboundReplayGates.get(addressKey);
58069
58376
  let replaySummary;
58070
58377
  try {
@@ -58083,8 +58390,21 @@ var XmtpService = class _XmtpService {
58083
58390
  recycled++;
58084
58391
  } catch (err) {
58085
58392
  failed++;
58086
- this.cancelStreamRecoveryForAddress(address);
58087
- this.clients.delete(address);
58393
+ if (newAgent && !newAgentReleased) {
58394
+ try {
58395
+ await this.releaseClient(
58396
+ address,
58397
+ newAgent,
58398
+ agentInfo.agentId,
58399
+ "recycle/create-failed"
58400
+ );
58401
+ } catch (releaseErr) {
58402
+ logWithTimestamp(
58403
+ `[xmtp-sdk] recycle: failed to release new client: ${address}`,
58404
+ releaseErr
58405
+ );
58406
+ }
58407
+ }
58088
58408
  logWithTimestamp(`[xmtp-sdk] recycle: failed to recreate client: ${address}`, err);
58089
58409
  logger.error(
58090
58410
  LogEvent.XMTP_CONNECTION_FAILED,
@@ -58488,7 +58808,7 @@ var XmtpService = class _XmtpService {
58488
58808
  }
58489
58809
  }
58490
58810
  async startListening() {
58491
- await this.startListeningForAddresses([...this.clients.keys()]);
58811
+ await this.startListeningForAddresses([...this.clients.keys()], "listen-failed");
58492
58812
  }
58493
58813
  async drainInboundReplayGate(address, expectedGate) {
58494
58814
  if (!expectedGate) {
@@ -58553,11 +58873,22 @@ var XmtpService = class _XmtpService {
58553
58873
  );
58554
58874
  return gate;
58555
58875
  }
58556
- cancelStreamRecoveryForAddress(address) {
58876
+ // With `client`, only that client's recovery is cancelled, so a late caller
58877
+ // cannot cancel the recovery of a newer instance on the same address.
58878
+ cancelStreamRecoveryForAddress(address, client) {
58557
58879
  const addressKey = address.toLowerCase();
58558
- const cleanup = this.streamRecoveryCleanupByAddress.get(addressKey);
58880
+ const entry = this.streamRecoveryCleanupByAddress.get(addressKey);
58881
+ if (!entry || client && entry.client !== client) {
58882
+ return;
58883
+ }
58559
58884
  this.streamRecoveryCleanupByAddress.delete(addressKey);
58560
- cleanup?.();
58885
+ entry.cleanup();
58886
+ }
58887
+ // True while `client` is the registered client of `address` (case-insensitive)
58888
+ // and the address is neither stopped nor being released.
58889
+ isCurrentListenOwner(address, client) {
58890
+ const key = address.toLowerCase();
58891
+ return this.getClientByAddress(address) === client && !this.stoppedAddresses.has(key) && !this.pendingReleaseByAddress.has(key);
58561
58892
  }
58562
58893
  getClientGeneration(client) {
58563
58894
  const existing = this.clientGenerationByClient.get(client);
@@ -58709,10 +59040,17 @@ var XmtpService = class _XmtpService {
58709
59040
  this.notifyAgentsWakeupAfterReplay();
58710
59041
  return summary;
58711
59042
  }
58712
- async startListeningForAddresses(addresses) {
59043
+ // Starts (at most once per client) the listener of every registered address
59044
+ // and reports each outcome. A "failed" client, i.e. one with neither a live
59045
+ // listener nor an owned retry task, is released with `releasePhase` before
59046
+ // this returns, so it is never served as a healthy instance.
59047
+ async startListeningForAddresses(addresses, releasePhase = "listen-failed") {
58713
59048
  const valid = addresses.filter((addr) => this.clients.has(addr));
58714
- await parallelMap(valid, async (address) => {
58715
- const agent = this.clients.get(address);
59049
+ const targets = valid.map((address) => ({
59050
+ address,
59051
+ client: this.clients.get(address)
59052
+ }));
59053
+ const listenForClient = async (address, agent) => {
58716
59054
  const clientGeneration = this.getClientGeneration(agent);
58717
59055
  const addressKey = address.toLowerCase();
58718
59056
  const tag = `[xmtp-sdk:${address}]`;
@@ -58723,6 +59061,11 @@ var XmtpService = class _XmtpService {
58723
59061
  onchainosAgentId: onchainosAgent?.agentId,
58724
59062
  role: onchainosAgent?.role
58725
59063
  };
59064
+ const ownsAddress = () => this.isCurrentListenOwner(address, agent);
59065
+ if (!ownsAddress()) {
59066
+ logWithTimestamp(`${tag} listener setup skipped: client no longer current`);
59067
+ return "skipped";
59068
+ }
58726
59069
  try {
58727
59070
  await agent.client.conversations.syncAll([
58728
59071
  import_node_bindings2.ConsentState.Allowed,
@@ -58730,13 +59073,21 @@ var XmtpService = class _XmtpService {
58730
59073
  ]);
58731
59074
  logWithTimestamp(`${tag} conversations synced (non-Denied)`);
58732
59075
  } catch (err) {
58733
- logWithTimestamp(`${tag} conversation sync failed, skipping this client:`, err);
59076
+ if (!ownsAddress()) {
59077
+ logWithTimestamp(`${tag} stale conversation sync failed after replacement, ignored:`, err);
59078
+ return "skipped";
59079
+ }
59080
+ logWithTimestamp(`${tag} conversation sync failed, listener not started:`, err);
58734
59081
  logger.error(
58735
59082
  LogEvent.CONVERSATIONS_STREAM_FAILED,
58736
59083
  err instanceof Error ? err : void 0,
58737
59084
  agentExtras(identity)
58738
59085
  );
58739
- return;
59086
+ return "failed";
59087
+ }
59088
+ if (!ownsAddress()) {
59089
+ logWithTimestamp(`${tag} listener setup skipped after sync: client no longer current`);
59090
+ return "skipped";
58740
59091
  }
58741
59092
  agent.use(loggingMiddleware(identity));
58742
59093
  agent.use(selfFilterMiddleware());
@@ -58765,6 +59116,7 @@ var XmtpService = class _XmtpService {
58765
59116
  });
58766
59117
  const gate = new InboundReplayGate(inbound);
58767
59118
  this.inboundReplayGates.set(address.toLowerCase(), gate);
59119
+ this.inboundReplayGateOwners.set(gate, agent);
58768
59120
  agent.use(gate.middleware);
58769
59121
  } else {
58770
59122
  logWithTimestamp(
@@ -58782,16 +59134,25 @@ var XmtpService = class _XmtpService {
58782
59134
  const MAX_RETRIES = 5;
58783
59135
  const BASE_DELAY_MS = 1e3;
58784
59136
  const startWithRetry = async (attempt = 0) => {
58785
- if (this.stoppedAddresses.has(address.toLowerCase()) || !this.clients.has(address)) {
59137
+ if (!ownsAddress()) {
58786
59138
  logWithTimestamp(`${tag} start skipped: client stopped or removed`);
58787
- return;
59139
+ return "skipped";
58788
59140
  }
58789
59141
  try {
58790
59142
  const liveStreamOptions = {
58791
59143
  consentStates: [import_node_bindings2.ConsentState.Allowed, import_node_bindings2.ConsentState.Unknown]
58792
59144
  };
58793
59145
  await agent.start(liveStreamOptions);
59146
+ if (!ownsAddress()) {
59147
+ logWithTimestamp(`${tag} stale agent.start resolved after replacement, ignored`);
59148
+ return "skipped";
59149
+ }
59150
+ return "started";
58794
59151
  } catch (err) {
59152
+ if (!ownsAddress()) {
59153
+ logWithTimestamp(`${tag} stale agent.start failed after replacement, ignored:`, err);
59154
+ return "skipped";
59155
+ }
58795
59156
  logWithTimestamp(`${tag} agent.start error (attempt=${attempt}):`, err);
58796
59157
  logger.error(
58797
59158
  LogEvent.AGENT_START_FAILED,
@@ -58801,17 +59162,27 @@ var XmtpService = class _XmtpService {
58801
59162
  attempt: String(attempt)
58802
59163
  }
58803
59164
  );
58804
- scheduleRetry(attempt);
59165
+ return scheduleRetry(attempt);
58805
59166
  }
58806
59167
  };
58807
59168
  const scheduleRetry = (attempt) => {
58808
- if (this.stoppedAddresses.has(address.toLowerCase()) || !this.clients.has(address)) {
59169
+ if (!ownsAddress()) {
58809
59170
  logWithTimestamp(`${tag} reconnect skipped: client stopped or removed`);
58810
- return;
59171
+ return "skipped";
58811
59172
  }
58812
59173
  if (attempt >= MAX_RETRIES) {
58813
- logWithTimestamp(`${tag} reached max retries (${MAX_RETRIES}), giving up reconnect`);
58814
- return;
59174
+ logWithTimestamp(
59175
+ `${tag} reached max retries (${MAX_RETRIES}), giving up reconnect; releasing client`
59176
+ );
59177
+ void this.releaseClient(
59178
+ address,
59179
+ agent,
59180
+ identity.onchainosAgentId ?? "",
59181
+ "listen/retry-exhausted"
59182
+ ).catch((err) => {
59183
+ logWithTimestamp(`${tag} release after retry exhaustion failed:`, err);
59184
+ });
59185
+ return "failed";
58815
59186
  }
58816
59187
  const delay = BASE_DELAY_MS * 2 ** attempt;
58817
59188
  logWithTimestamp(`${tag} reconnecting in ${delay}ms (attempt=${attempt + 1})`);
@@ -58819,6 +59190,7 @@ var XmtpService = class _XmtpService {
58819
59190
  () => void startWithRetry(attempt + 1),
58820
59191
  delay
58821
59192
  );
59193
+ return "retrying";
58822
59194
  };
58823
59195
  const streamRecovery = new StreamRecoveryTelemetry();
58824
59196
  let recoveryTimeout;
@@ -58838,10 +59210,10 @@ var XmtpService = class _XmtpService {
58838
59210
  recoveryStabilityTimeout = void 0;
58839
59211
  }
58840
59212
  };
58841
- this.streamRecoveryCleanupByAddress.set(
58842
- addressKey,
58843
- cleanupStreamRecovery
58844
- );
59213
+ this.streamRecoveryCleanupByAddress.set(addressKey, {
59214
+ client: agent,
59215
+ cleanup: cleanupStreamRecovery
59216
+ });
58845
59217
  agent.on("unhandledError", (err) => {
58846
59218
  if (recoveryDisposed || this.clients.get(address) !== agent || this.stoppedAddresses.has(addressKey)) {
58847
59219
  return;
@@ -59003,9 +59375,54 @@ var XmtpService = class _XmtpService {
59003
59375
  `${tag} agent message event msg=${msg?.id ?? "(unknown)"} kind=${msg?.kind ?? "(unknown)"} conv=${conversation?.id ?? msg?.conversationId ?? "(unknown)"} sender=${msg?.senderInboxId ?? "(unknown)"} isText=${typeof ctx.isText === "function" ? String(ctx.isText()) : "unknown"} preview="${previewDebugContent(msg?.content)}"`
59004
59376
  );
59005
59377
  });
59006
- await startWithRetry();
59007
- logWithTimestamp(`${tag} message listener started`);
59378
+ const outcome = await startWithRetry();
59379
+ logWithTimestamp(`${tag} message listener ${outcome}`);
59380
+ return outcome;
59381
+ };
59382
+ const settled = await parallelMap(targets, async ({ address, client }) => {
59383
+ let task = this.listenTaskByClient.get(client);
59384
+ if (!task) {
59385
+ task = listenForClient(address, client);
59386
+ this.listenTaskByClient.set(client, task);
59387
+ }
59388
+ let outcome;
59389
+ try {
59390
+ outcome = await task;
59391
+ } catch (err) {
59392
+ logWithTimestamp(`[xmtp-sdk:${address}] listener setup failed:`, err);
59393
+ logger.error(
59394
+ LogEvent.AGENT_START_FAILED,
59395
+ err instanceof Error ? err : new Error(String(err)),
59396
+ {
59397
+ ...agentExtras({
59398
+ walletAddress: address,
59399
+ inboxId: client.client?.inboxId,
59400
+ onchainosAgentId: this.getAgentByAddress(address)?.agentId
59401
+ }),
59402
+ phase: "listener-setup"
59403
+ }
59404
+ );
59405
+ outcome = "failed";
59406
+ }
59407
+ if (outcome === "failed") {
59408
+ await this.releaseClient(
59409
+ address,
59410
+ client,
59411
+ this.getAgentByAddress(address)?.agentId ?? "",
59412
+ releasePhase
59413
+ );
59414
+ }
59415
+ return outcome;
59416
+ });
59417
+ const results = /* @__PURE__ */ new Map();
59418
+ targets.forEach(({ address, client }, i) => {
59419
+ const r = settled[i];
59420
+ results.set(address, {
59421
+ client,
59422
+ outcome: r.status === "fulfilled" ? r.value : "failed"
59423
+ });
59008
59424
  });
59425
+ return results;
59009
59426
  }
59010
59427
  async replayOfflineMessagesForAddress(address, expectedClient, trigger = "periodic_repair", recoveryIdentity) {
59011
59428
  const addressReplayStartedAt = Date.now();
@@ -61716,7 +62133,7 @@ function buildConnectParams(config, instanceId, protocolVersion) {
61716
62133
  client: {
61717
62134
  id: "gateway-client",
61718
62135
  displayName: "okx-a2a-node",
61719
- version: "0.2.17",
62136
+ version: "0.2.18",
61720
62137
  platform: "node",
61721
62138
  mode: "backend",
61722
62139
  instanceId
@@ -61727,7 +62144,7 @@ function buildConnectParams(config, instanceId, protocolVersion) {
61727
62144
  commands: [],
61728
62145
  permissions: {},
61729
62146
  locale: Intl.DateTimeFormat().resolvedOptions().locale || "en-US",
61730
- userAgent: `okx-a2a-node/${"0.2.17"}`,
62147
+ userAgent: `okx-a2a-node/${"0.2.18"}`,
61731
62148
  auth: {
61732
62149
  ...config.token ? { token: config.token } : {},
61733
62150
  ...config.password ? { password: config.password } : {}
@@ -68067,12 +68484,12 @@ async function runListenerWithLock(options, paths) {
68067
68484
  });
68068
68485
  }
68069
68486
  });
68070
- service.setPluginVersion("0.2.17");
68487
+ service.setPluginVersion("0.2.18");
68071
68488
  await service.init();
68072
68489
  const pluginVersionStatus = service.pluginVersionStatus;
68073
68490
  if (pluginVersionStatus.unavailable) {
68074
68491
  throw new Error(
68075
- `@okxweb3/a2a-node v${"0.2.17"} is below the required minimum v${pluginVersionStatus.minVersion}`
68492
+ `@okxweb3/a2a-node v${"0.2.18"} is below the required minimum v${pluginVersionStatus.minVersion}`
68076
68493
  );
68077
68494
  }
68078
68495
  const systemConfig = service.getSystemConfig();
@@ -68100,7 +68517,7 @@ async function runListenerWithLock(options, paths) {
68100
68517
  onchainosAgentId: "*",
68101
68518
  reason: "system-config missing sentryDsn",
68102
68519
  pluginId: "@okxweb3/a2a-node",
68103
- pluginVersion: "0.2.17"
68520
+ pluginVersion: "0.2.18"
68104
68521
  });
68105
68522
  }
68106
68523
  logWithTimestamp(
@@ -70873,7 +71290,7 @@ async function exportDiagnosticLogs(options) {
70873
71290
  node: process.version,
70874
71291
  platform: process.platform,
70875
71292
  arch: process.arch,
70876
- packageVersion: true ? "0.2.17" : "unknown",
71293
+ packageVersion: true ? "0.2.18" : "unknown",
70877
71294
  sensitiveContentIncluded: options.includeSensitiveContent,
70878
71295
  listenerAndLlmContentIncluded: true,
70879
71296
  credentialsAlwaysRedacted: true,
@@ -72785,7 +73202,7 @@ async function runDoctor(options = {}) {
72785
73202
  platform: options.platform ?? process.platform,
72786
73203
  env: options.env ?? process.env,
72787
73204
  target: options.target ?? resolveDoctorTarget(options.env ?? process.env),
72788
- cliVersion: options.cliVersion ?? (true ? "0.2.17" : "0.0.0"),
73205
+ cliVersion: options.cliVersion ?? (true ? "0.2.18" : "0.0.0"),
72789
73206
  fixMode: options.fix === true,
72790
73207
  nonInteractive: options.nonInteractive === true,
72791
73208
  packageChanged: false,