clawdentity 0.0.24 → 0.0.26

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.
package/dist/index.js CHANGED
@@ -14361,6 +14361,9 @@ var INVITES_REDEEM_PATH = "/v1/invites/redeem";
14361
14361
  var ME_API_KEYS_PATH = "/v1/me/api-keys";
14362
14362
  var REGISTRY_METADATA_PATH = "/v1/metadata";
14363
14363
  var RELAY_CONNECT_PATH = "/v1/relay/connect";
14364
+ var RELAY_DELIVERY_RECEIPTS_PATH = "/v1/relay/delivery-receipts";
14365
+ var RELAY_CONVERSATION_ID_HEADER = "x-claw-conversation-id";
14366
+ var RELAY_DELIVERY_RECEIPT_URL_HEADER = "x-claw-delivery-receipt-url";
14364
14367
  var RELAY_RECIPIENT_AGENT_DID_HEADER = "x-claw-recipient-agent-did";
14365
14368
 
14366
14369
  // ../../packages/protocol/src/http-signing.ts
@@ -14376,6 +14379,24 @@ function canonicalizeRequest(input) {
14376
14379
  ].join("\n");
14377
14380
  }
14378
14381
 
14382
+ // ../../packages/sdk/src/datetime.ts
14383
+ function toDate(value) {
14384
+ const parsed = value instanceof Date ? value : new Date(value);
14385
+ if (Number.isNaN(parsed.getTime())) {
14386
+ throw new TypeError(`Invalid datetime value: ${String(value)}`);
14387
+ }
14388
+ return parsed;
14389
+ }
14390
+ function nowUtcMs() {
14391
+ return Date.now();
14392
+ }
14393
+ function toIso(value) {
14394
+ return toDate(value).toISOString();
14395
+ }
14396
+ function nowIso() {
14397
+ return toIso(nowUtcMs());
14398
+ }
14399
+
14379
14400
  // ../../node_modules/.pnpm/hono@4.11.9/node_modules/hono/dist/request/constants.js
14380
14401
  var GET_MATCH_RESULT = /* @__PURE__ */ Symbol();
14381
14402
 
@@ -15573,7 +15594,7 @@ async function refreshAgentAuthWithClawProof(input) {
15573
15594
  const refreshBody = JSON.stringify({
15574
15595
  refreshToken: input.refreshToken
15575
15596
  });
15576
- const nowMs = input.nowMs?.() ?? Date.now();
15597
+ const nowMs = input.nowMs?.() ?? nowUtcMs();
15577
15598
  const timestamp = String(Math.floor(nowMs / 1e3));
15578
15599
  const nonce = encodeBase64url(crypto.getRandomValues(new Uint8Array(16)));
15579
15600
  const signed = await signHttpRequest({
@@ -15780,11 +15801,6 @@ function parseRegistryConfig(env, options = {}) {
15780
15801
  var DEFAULT_CRL_REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
15781
15802
  var DEFAULT_CRL_MAX_AGE_MS = 15 * 60 * 1e3;
15782
15803
 
15783
- // ../../packages/sdk/src/datetime.ts
15784
- function nowIso() {
15785
- return (/* @__PURE__ */ new Date()).toISOString();
15786
- }
15787
-
15788
15804
  // ../../node_modules/.pnpm/jose@6.1.3/node_modules/jose/dist/webapi/lib/buffer_utils.js
15789
15805
  var encoder = new TextEncoder();
15790
15806
  var decoder = new TextDecoder();
@@ -17840,7 +17856,7 @@ var parseAgentIdFromDid = (agentName, did) => {
17840
17856
  }
17841
17857
  };
17842
17858
  var formatExpiresAt = (expires) => {
17843
- return new Date(expires * 1e3).toISOString();
17859
+ return toIso(expires * 1e3);
17844
17860
  };
17845
17861
  var resolveFramework = (framework) => {
17846
17862
  if (framework === void 0) {
@@ -17991,7 +18007,7 @@ var writeSecureFile2 = async (path, content) => {
17991
18007
  await chmod2(path, FILE_MODE2);
17992
18008
  };
17993
18009
  var writeSecureFileAtomically = async (path, content) => {
17994
- const tempPath = `${path}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
18010
+ const tempPath = `${path}.tmp-${nowUtcMs()}-${Math.random().toString(16).slice(2)}`;
17995
18011
  await writeFile2(tempPath, content, "utf-8");
17996
18012
  await chmod2(tempPath, FILE_MODE2);
17997
18013
  try {
@@ -18936,7 +18952,9 @@ var DEFAULT_OPENCLAW_DELIVER_MAX_ATTEMPTS = 4;
18936
18952
  var DEFAULT_OPENCLAW_DELIVER_RETRY_INITIAL_DELAY_MS = 300;
18937
18953
  var DEFAULT_OPENCLAW_DELIVER_RETRY_MAX_DELAY_MS = 2e3;
18938
18954
  var DEFAULT_OPENCLAW_DELIVER_RETRY_BACKOFF_FACTOR = 2;
18955
+ var DEFAULT_CONNECT_TIMEOUT_MS = 15e3;
18939
18956
  var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
18957
+ var DEFAULT_HEARTBEAT_ACK_TIMEOUT_MS = 6e4;
18940
18958
  var DEFAULT_RECONNECT_MIN_DELAY_MS = 1e3;
18941
18959
  var DEFAULT_RECONNECT_MAX_DELAY_MS = 3e4;
18942
18960
  var DEFAULT_RECONNECT_BACKOFF_FACTOR = 2;
@@ -18953,6 +18971,15 @@ var DEFAULT_CONNECTOR_INBOUND_REPLAY_BATCH_SIZE = 25;
18953
18971
  var DEFAULT_CONNECTOR_INBOUND_RETRY_INITIAL_DELAY_MS = 1e3;
18954
18972
  var DEFAULT_CONNECTOR_INBOUND_RETRY_MAX_DELAY_MS = 6e4;
18955
18973
  var DEFAULT_CONNECTOR_INBOUND_RETRY_BACKOFF_FACTOR = 2;
18974
+ var DEFAULT_CONNECTOR_INBOUND_EVENTS_MAX_BYTES = 10 * 1024 * 1024;
18975
+ var DEFAULT_CONNECTOR_INBOUND_EVENTS_MAX_FILES = 5;
18976
+ var DEFAULT_CONNECTOR_INBOUND_DEAD_LETTER_NON_RETRYABLE_MAX_ATTEMPTS = 5;
18977
+ var DEFAULT_CONNECTOR_OPENCLAW_PROBE_INTERVAL_MS = 1e4;
18978
+ var DEFAULT_CONNECTOR_OPENCLAW_PROBE_TIMEOUT_MS = 3e3;
18979
+ var DEFAULT_CONNECTOR_RUNTIME_REPLAY_DELIVER_MAX_ATTEMPTS = 3;
18980
+ var DEFAULT_CONNECTOR_RUNTIME_REPLAY_DELIVER_RETRY_INITIAL_DELAY_MS = 2e3;
18981
+ var DEFAULT_CONNECTOR_RUNTIME_REPLAY_DELIVER_RETRY_MAX_DELAY_MS = 8e3;
18982
+ var DEFAULT_CONNECTOR_RUNTIME_REPLAY_DELIVER_RETRY_BACKOFF_FACTOR = 2;
18956
18983
  var AGENT_ACCESS_HEADER = "x-claw-agent-access";
18957
18984
  var WS_READY_STATE_OPEN = 1;
18958
18985
 
@@ -19114,6 +19141,7 @@ function serializeFrame(frame) {
19114
19141
  }
19115
19142
 
19116
19143
  // ../../packages/connector/src/client.ts
19144
+ var WS_READY_STATE_CONNECTING = 0;
19117
19145
  function isAbortError(error48) {
19118
19146
  return error48 instanceof Error && error48.name === "AbortError";
19119
19147
  }
@@ -19178,6 +19206,33 @@ function readCloseEvent(event) {
19178
19206
  wasClean: typeof event.wasClean === "boolean" ? event.wasClean : false
19179
19207
  };
19180
19208
  }
19209
+ function readUnexpectedResponseStatus(event) {
19210
+ if (!isObject3(event)) {
19211
+ return void 0;
19212
+ }
19213
+ if (typeof event.status === "number") {
19214
+ return event.status;
19215
+ }
19216
+ if (typeof event.statusCode === "number") {
19217
+ return event.statusCode;
19218
+ }
19219
+ const response = event.response;
19220
+ if (isObject3(response)) {
19221
+ if (typeof response.status === "number") {
19222
+ return response.status;
19223
+ }
19224
+ if (typeof response.statusCode === "number") {
19225
+ return response.statusCode;
19226
+ }
19227
+ }
19228
+ return void 0;
19229
+ }
19230
+ function readErrorEventReason(event) {
19231
+ if (!isObject3(event) || !("error" in event)) {
19232
+ return "WebSocket error";
19233
+ }
19234
+ return sanitizeErrorReason(event.error);
19235
+ }
19181
19236
  function normalizeConnectionHeaders(headers) {
19182
19237
  if (headers === void 0) {
19183
19238
  return {};
@@ -19199,7 +19254,9 @@ var ConnectorClient = class {
19199
19254
  connectionHeadersProvider;
19200
19255
  openclawHookUrl;
19201
19256
  openclawHookToken;
19257
+ connectTimeoutMs;
19202
19258
  heartbeatIntervalMs;
19259
+ heartbeatAckTimeoutMs;
19203
19260
  reconnectMinDelayMs;
19204
19261
  reconnectMaxDelayMs;
19205
19262
  reconnectBackoffFactor;
@@ -19214,14 +19271,36 @@ var ConnectorClient = class {
19214
19271
  fetchImpl;
19215
19272
  logger;
19216
19273
  hooks;
19274
+ outboundQueuePersistence;
19217
19275
  inboundDeliverHandler;
19218
19276
  now;
19219
19277
  random;
19220
19278
  ulidFactory;
19221
19279
  socket;
19222
19280
  reconnectTimeout;
19281
+ connectTimeout;
19223
19282
  heartbeatInterval;
19283
+ heartbeatAckTimeout;
19284
+ pendingHeartbeatAcks = /* @__PURE__ */ new Map();
19224
19285
  reconnectAttempt = 0;
19286
+ reconnectCount = 0;
19287
+ connectAttempts = 0;
19288
+ connectedSinceMs;
19289
+ accumulatedConnectedMs = 0;
19290
+ lastConnectedAtIso;
19291
+ heartbeatRttSampleCount = 0;
19292
+ heartbeatRttTotalMs = 0;
19293
+ heartbeatRttMaxMs = 0;
19294
+ heartbeatRttLastMs;
19295
+ inboundAckLatencySampleCount = 0;
19296
+ inboundAckLatencyTotalMs = 0;
19297
+ inboundAckLatencyMaxMs = 0;
19298
+ inboundAckLatencyLastMs;
19299
+ maxObservedOutboundQueueDepth = 0;
19300
+ outboundQueueLoaded = false;
19301
+ outboundQueueLoadPromise;
19302
+ outboundQueueSaveChain = Promise.resolve();
19303
+ authUpgradeImmediateRetryUsed = false;
19225
19304
  started = false;
19226
19305
  outboundQueue = [];
19227
19306
  constructor(options) {
@@ -19231,7 +19310,17 @@ var ConnectorClient = class {
19231
19310
  );
19232
19311
  this.connectionHeadersProvider = options.connectionHeadersProvider;
19233
19312
  this.openclawHookToken = options.openclawHookToken;
19313
+ this.connectTimeoutMs = Math.max(
19314
+ 0,
19315
+ Math.floor(options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS)
19316
+ );
19234
19317
  this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
19318
+ this.heartbeatAckTimeoutMs = Math.max(
19319
+ 0,
19320
+ Math.floor(
19321
+ options.heartbeatAckTimeoutMs ?? DEFAULT_HEARTBEAT_ACK_TIMEOUT_MS
19322
+ )
19323
+ );
19235
19324
  this.reconnectMinDelayMs = options.reconnectMinDelayMs ?? DEFAULT_RECONNECT_MIN_DELAY_MS;
19236
19325
  this.reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
19237
19326
  this.reconnectBackoffFactor = options.reconnectBackoffFactor ?? DEFAULT_RECONNECT_BACKOFF_FACTOR;
@@ -19269,6 +19358,7 @@ var ConnectorClient = class {
19269
19358
  this.fetchImpl = options.fetchImpl ?? fetch;
19270
19359
  this.logger = options.logger ?? createLogger({ service: "connector", module: "client" });
19271
19360
  this.hooks = options.hooks ?? {};
19361
+ this.outboundQueuePersistence = options.outboundQueuePersistence;
19272
19362
  this.inboundDeliverHandler = options.inboundDeliverHandler;
19273
19363
  this.now = options.now ?? Date.now;
19274
19364
  this.random = options.random ?? Math.random;
@@ -19283,16 +19373,19 @@ var ConnectorClient = class {
19283
19373
  return;
19284
19374
  }
19285
19375
  this.started = true;
19376
+ if (this.outboundQueuePersistence !== void 0) {
19377
+ void this.ensureOutboundQueueLoaded();
19378
+ }
19286
19379
  void this.connectSocket();
19287
19380
  }
19288
19381
  disconnect() {
19289
19382
  this.started = false;
19290
19383
  this.clearReconnectTimeout();
19291
- this.clearHeartbeatInterval();
19384
+ this.clearSocketState();
19292
19385
  if (this.socket !== void 0) {
19293
19386
  const socket = this.socket;
19294
19387
  this.socket = void 0;
19295
- socket.close(1e3, "client disconnect");
19388
+ this.closeSocketQuietly(socket, 1e3, "client disconnect");
19296
19389
  }
19297
19390
  }
19298
19391
  isConnected() {
@@ -19301,6 +19394,42 @@ var ConnectorClient = class {
19301
19394
  getQueuedOutboundCount() {
19302
19395
  return this.outboundQueue.length;
19303
19396
  }
19397
+ getMetricsSnapshot() {
19398
+ const nowMs = this.now();
19399
+ const uptimeMs = this.accumulatedConnectedMs + (this.connectedSinceMs === void 0 ? 0 : nowMs - this.connectedSinceMs);
19400
+ return {
19401
+ connection: {
19402
+ connectAttempts: this.connectAttempts,
19403
+ connected: this.isConnected(),
19404
+ reconnectCount: this.reconnectCount,
19405
+ uptimeMs: Math.max(0, uptimeMs),
19406
+ lastConnectedAt: this.lastConnectedAtIso
19407
+ },
19408
+ heartbeat: {
19409
+ pendingAckCount: this.pendingHeartbeatAcks.size,
19410
+ sampleCount: this.heartbeatRttSampleCount,
19411
+ lastRttMs: this.heartbeatRttLastMs,
19412
+ maxRttMs: this.heartbeatRttSampleCount > 0 ? this.heartbeatRttMaxMs : void 0,
19413
+ avgRttMs: this.heartbeatRttSampleCount > 0 ? Math.floor(
19414
+ this.heartbeatRttTotalMs / this.heartbeatRttSampleCount
19415
+ ) : void 0
19416
+ },
19417
+ inboundDelivery: {
19418
+ sampleCount: this.inboundAckLatencySampleCount,
19419
+ lastAckLatencyMs: this.inboundAckLatencyLastMs,
19420
+ maxAckLatencyMs: this.inboundAckLatencySampleCount > 0 ? this.inboundAckLatencyMaxMs : void 0,
19421
+ avgAckLatencyMs: this.inboundAckLatencySampleCount > 0 ? Math.floor(
19422
+ this.inboundAckLatencyTotalMs / this.inboundAckLatencySampleCount
19423
+ ) : void 0
19424
+ },
19425
+ outboundQueue: {
19426
+ currentDepth: this.outboundQueue.length,
19427
+ maxDepth: this.maxObservedOutboundQueueDepth,
19428
+ loadedFromPersistence: this.outboundQueueLoaded,
19429
+ persistenceEnabled: this.outboundQueuePersistence !== void 0
19430
+ }
19431
+ };
19432
+ }
19304
19433
  enqueueOutbound(input) {
19305
19434
  const frame = enqueueFrameSchema.parse({
19306
19435
  v: CONNECTOR_FRAME_VERSION,
@@ -19313,11 +19442,17 @@ var ConnectorClient = class {
19313
19442
  replyTo: input.replyTo
19314
19443
  });
19315
19444
  this.outboundQueue.push(frame);
19445
+ this.recordOutboundQueueDepth();
19446
+ this.persistOutboundQueue();
19316
19447
  this.flushOutboundQueue();
19317
19448
  return frame;
19318
19449
  }
19319
19450
  async connectSocket() {
19320
19451
  this.clearReconnectTimeout();
19452
+ this.connectAttempts += 1;
19453
+ if (this.outboundQueuePersistence !== void 0) {
19454
+ await this.ensureOutboundQueueLoaded();
19455
+ }
19321
19456
  let connectionHeaders = this.connectionHeaders;
19322
19457
  if (this.connectionHeadersProvider) {
19323
19458
  try {
@@ -19344,8 +19479,18 @@ var ConnectorClient = class {
19344
19479
  this.scheduleReconnect();
19345
19480
  return;
19346
19481
  }
19347
- this.socket.addEventListener("open", () => {
19482
+ const socket = this.socket;
19483
+ this.startConnectTimeout(socket);
19484
+ socket.addEventListener("open", () => {
19485
+ if (this.socket !== socket) {
19486
+ return;
19487
+ }
19488
+ this.clearConnectTimeout();
19489
+ this.clearHeartbeatTracking();
19348
19490
  this.reconnectAttempt = 0;
19491
+ this.authUpgradeImmediateRetryUsed = false;
19492
+ this.connectedSinceMs = this.now();
19493
+ this.lastConnectedAtIso = this.makeTimestamp();
19349
19494
  this.logger.info("connector.websocket.connected", {
19350
19495
  url: this.connectorUrl
19351
19496
  });
@@ -19353,12 +19498,16 @@ var ConnectorClient = class {
19353
19498
  this.flushOutboundQueue();
19354
19499
  this.hooks.onConnected?.();
19355
19500
  });
19356
- this.socket.addEventListener("message", (event) => {
19501
+ socket.addEventListener("message", (event) => {
19502
+ if (this.socket !== socket) {
19503
+ return;
19504
+ }
19357
19505
  void this.handleIncomingMessage(readMessageEventData(event));
19358
19506
  });
19359
- this.socket.addEventListener("close", (event) => {
19360
- this.clearHeartbeatInterval();
19361
- this.socket = void 0;
19507
+ socket.addEventListener("close", (event) => {
19508
+ if (!this.detachSocket(socket)) {
19509
+ return;
19510
+ }
19362
19511
  const closeEvent = readCloseEvent(event);
19363
19512
  this.logger.warn("connector.websocket.closed", {
19364
19513
  closeCode: closeEvent.code,
@@ -19374,22 +19523,61 @@ var ConnectorClient = class {
19374
19523
  this.scheduleReconnect();
19375
19524
  }
19376
19525
  });
19377
- this.socket.addEventListener("error", () => {
19526
+ socket.addEventListener("error", (event) => {
19527
+ if (this.socket !== socket) {
19528
+ return;
19529
+ }
19530
+ const readyState = socket.readyState;
19531
+ const shouldForceReconnect = readyState !== WS_READY_STATE_OPEN && readyState !== WS_READY_STATE_CONNECTING;
19532
+ if (!shouldForceReconnect) {
19533
+ this.logger.warn("connector.websocket.error", {
19534
+ url: this.connectorUrl,
19535
+ reason: readErrorEventReason(event),
19536
+ readyState
19537
+ });
19538
+ return;
19539
+ }
19540
+ if (!this.detachSocket(socket)) {
19541
+ return;
19542
+ }
19543
+ const reason = readErrorEventReason(event);
19378
19544
  this.logger.warn("connector.websocket.error", {
19379
- url: this.connectorUrl
19545
+ url: this.connectorUrl,
19546
+ reason
19380
19547
  });
19548
+ this.closeSocketQuietly(socket, 1011, "websocket error");
19549
+ this.hooks.onDisconnected?.({
19550
+ code: 1006,
19551
+ reason,
19552
+ wasClean: false
19553
+ });
19554
+ if (this.started) {
19555
+ this.scheduleReconnect();
19556
+ }
19557
+ });
19558
+ socket.addEventListener("unexpected-response", (event) => {
19559
+ void this.handleUnexpectedResponse(socket, event);
19381
19560
  });
19382
19561
  }
19383
- scheduleReconnect() {
19562
+ scheduleReconnect(options) {
19384
19563
  if (!this.started) {
19385
19564
  return;
19386
19565
  }
19387
- const exponentialDelay = this.reconnectMinDelayMs * this.reconnectBackoffFactor ** this.reconnectAttempt;
19388
- const boundedDelay = Math.min(exponentialDelay, this.reconnectMaxDelayMs);
19389
- const jitterRange = boundedDelay * this.reconnectJitterRatio;
19390
- const jitterOffset = jitterRange === 0 ? 0 : (this.random() * 2 - 1) * jitterRange;
19391
- const delayMs = Math.max(0, Math.floor(boundedDelay + jitterOffset));
19392
- this.reconnectAttempt += 1;
19566
+ this.clearReconnectTimeout();
19567
+ let delayMs;
19568
+ if (options?.delayMs !== void 0) {
19569
+ delayMs = Math.max(0, Math.floor(options.delayMs));
19570
+ } else {
19571
+ const exponentialDelay = this.reconnectMinDelayMs * this.reconnectBackoffFactor ** this.reconnectAttempt;
19572
+ const boundedDelay = Math.min(exponentialDelay, this.reconnectMaxDelayMs);
19573
+ const jitterRange = boundedDelay * this.reconnectJitterRatio;
19574
+ const jitterOffset = jitterRange === 0 ? 0 : (this.random() * 2 - 1) * jitterRange;
19575
+ delayMs = Math.max(0, Math.floor(boundedDelay + jitterOffset));
19576
+ }
19577
+ if (options?.incrementAttempt ?? true) {
19578
+ this.reconnectAttempt += 1;
19579
+ }
19580
+ this.reconnectCount += 1;
19393
19581
  this.reconnectTimeout = setTimeout(() => {
19394
19582
  void this.connectSocket();
19395
19583
  }, delayMs);
@@ -19400,8 +19588,124 @@ var ConnectorClient = class {
19400
19588
  this.reconnectTimeout = void 0;
19401
19589
  }
19402
19590
  }
19591
+ startConnectTimeout(socket) {
19592
+ this.clearConnectTimeout();
19593
+ if (this.connectTimeoutMs <= 0) {
19594
+ return;
19595
+ }
19596
+ this.connectTimeout = setTimeout(() => {
19597
+ if (!this.detachSocket(socket)) {
19598
+ return;
19599
+ }
19600
+ this.logger.warn("connector.websocket.connect_timeout", {
19601
+ timeoutMs: this.connectTimeoutMs,
19602
+ url: this.connectorUrl
19603
+ });
19604
+ this.closeSocketQuietly(socket, 1e3, "connect timeout");
19605
+ this.hooks.onDisconnected?.({
19606
+ code: 1006,
19607
+ reason: "WebSocket connect timed out",
19608
+ wasClean: false
19609
+ });
19610
+ if (this.started) {
19611
+ this.scheduleReconnect();
19612
+ }
19613
+ }, this.connectTimeoutMs);
19614
+ }
19615
+ clearConnectTimeout() {
19616
+ if (this.connectTimeout !== void 0) {
19617
+ clearTimeout(this.connectTimeout);
19618
+ this.connectTimeout = void 0;
19619
+ }
19620
+ }
19621
+ clearSocketState() {
19622
+ this.clearConnectTimeout();
19623
+ this.clearHeartbeatTracking();
19624
+ }
19625
+ clearHeartbeatTracking() {
19626
+ if (this.heartbeatInterval !== void 0) {
19627
+ clearInterval(this.heartbeatInterval);
19628
+ this.heartbeatInterval = void 0;
19629
+ }
19630
+ if (this.heartbeatAckTimeout !== void 0) {
19631
+ clearTimeout(this.heartbeatAckTimeout);
19632
+ this.heartbeatAckTimeout = void 0;
19633
+ }
19634
+ this.pendingHeartbeatAcks.clear();
19635
+ }
19636
+ detachSocket(socket) {
19637
+ if (this.socket !== socket) {
19638
+ return false;
19639
+ }
19640
+ this.socket = void 0;
19641
+ if (this.connectedSinceMs !== void 0) {
19642
+ this.accumulatedConnectedMs += Math.max(
19643
+ 0,
19644
+ this.now() - this.connectedSinceMs
19645
+ );
19646
+ this.connectedSinceMs = void 0;
19647
+ }
19648
+ this.clearSocketState();
19649
+ return true;
19650
+ }
19651
+ closeSocketQuietly(socket, code, reason) {
19652
+ try {
19653
+ socket.close(code, reason);
19654
+ } catch (error48) {
19655
+ this.logger.warn("connector.websocket.close_failed", {
19656
+ reason: sanitizeErrorReason(error48)
19657
+ });
19658
+ }
19659
+ }
19660
+ async handleUnexpectedResponse(socket, event) {
19661
+ if (!this.detachSocket(socket)) {
19662
+ return;
19663
+ }
19664
+ const statusCode = readUnexpectedResponseStatus(event);
19665
+ const isAuthRejected = statusCode === 401;
19666
+ const immediateRetry = isAuthRejected && !this.authUpgradeImmediateRetryUsed;
19667
+ if (isAuthRejected) {
19668
+ this.authUpgradeImmediateRetryUsed = true;
19669
+ await this.invokeAuthUpgradeRejectedHook({
19670
+ status: 401,
19671
+ immediateRetry
19672
+ });
19673
+ }
19674
+ const reason = statusCode === void 0 ? "WebSocket upgrade rejected" : `WebSocket upgrade rejected with status ${statusCode}`;
19675
+ this.logger.warn("connector.websocket.unexpected_response", {
19676
+ statusCode,
19677
+ immediateRetry,
19678
+ url: this.connectorUrl
19679
+ });
19680
+ this.closeSocketQuietly(socket, 1e3, reason);
19681
+ this.hooks.onDisconnected?.({
19682
+ code: 1006,
19683
+ reason,
19684
+ wasClean: false
19685
+ });
19686
+ if (this.started) {
19687
+ this.scheduleReconnect(
19688
+ immediateRetry ? { delayMs: 0, incrementAttempt: false } : void 0
19689
+ );
19690
+ }
19691
+ }
19692
+ async invokeAuthUpgradeRejectedHook(input) {
19693
+ if (this.hooks.onAuthUpgradeRejected === void 0) {
19694
+ return;
19695
+ }
19696
+ try {
19697
+ await this.hooks.onAuthUpgradeRejected(input);
19698
+ } catch (error48) {
19699
+ this.logger.warn(
19700
+ "connector.websocket.auth_upgrade_rejected_hook_failed",
19701
+ {
19702
+ reason: sanitizeErrorReason(error48)
19703
+ }
19704
+ );
19705
+ }
19706
+ }
19403
19707
  startHeartbeatInterval() {
19404
- this.clearHeartbeatInterval();
19708
+ this.clearHeartbeatTracking();
19405
19709
  if (this.heartbeatIntervalMs <= 0) {
19406
19710
  return;
19407
19711
  }
@@ -19412,13 +19716,82 @@ var ConnectorClient = class {
19412
19716
  id: this.makeFrameId(),
19413
19717
  ts: this.makeTimestamp()
19414
19718
  };
19415
- this.sendFrame(frame);
19719
+ if (this.sendFrame(frame)) {
19720
+ this.trackHeartbeatAck(frame.id);
19721
+ }
19416
19722
  }, this.heartbeatIntervalMs);
19417
19723
  }
19418
- clearHeartbeatInterval() {
19419
- if (this.heartbeatInterval !== void 0) {
19420
- clearInterval(this.heartbeatInterval);
19421
- this.heartbeatInterval = void 0;
19724
+ trackHeartbeatAck(ackId) {
19725
+ if (this.heartbeatAckTimeoutMs <= 0) {
19726
+ return;
19727
+ }
19728
+ this.pendingHeartbeatAcks.set(ackId, this.now());
19729
+ this.scheduleHeartbeatAckTimeoutCheck();
19730
+ }
19731
+ handleHeartbeatAckFrame(frame) {
19732
+ const sentAtMs = this.pendingHeartbeatAcks.get(frame.ackId);
19733
+ if (sentAtMs === void 0) {
19734
+ return;
19735
+ }
19736
+ this.pendingHeartbeatAcks.delete(frame.ackId);
19737
+ const rttMs = Math.max(0, this.now() - sentAtMs);
19738
+ this.heartbeatRttSampleCount += 1;
19739
+ this.heartbeatRttTotalMs += rttMs;
19740
+ this.heartbeatRttMaxMs = Math.max(this.heartbeatRttMaxMs, rttMs);
19741
+ this.heartbeatRttLastMs = rttMs;
19742
+ this.scheduleHeartbeatAckTimeoutCheck();
19743
+ }
19744
+ scheduleHeartbeatAckTimeoutCheck() {
19745
+ if (this.heartbeatAckTimeout !== void 0) {
19746
+ clearTimeout(this.heartbeatAckTimeout);
19747
+ this.heartbeatAckTimeout = void 0;
19748
+ }
19749
+ if (this.pendingHeartbeatAcks.size === 0 || this.heartbeatAckTimeoutMs <= 0) {
19750
+ return;
19751
+ }
19752
+ let oldestSentAt = Number.POSITIVE_INFINITY;
19753
+ for (const sentAt of this.pendingHeartbeatAcks.values()) {
19754
+ oldestSentAt = Math.min(oldestSentAt, sentAt);
19755
+ }
19756
+ const elapsedMs = this.now() - oldestSentAt;
19757
+ const delayMs = Math.max(0, this.heartbeatAckTimeoutMs - elapsedMs);
19758
+ this.heartbeatAckTimeout = setTimeout(() => {
19759
+ this.heartbeatAckTimeout = void 0;
19760
+ this.handleHeartbeatAckTimeout();
19761
+ }, delayMs);
19762
+ }
19763
+ handleHeartbeatAckTimeout() {
19764
+ const pendingCount = this.pendingHeartbeatAcks.size;
19765
+ if (pendingCount === 0) {
19766
+ return;
19767
+ }
19768
+ let oldestSentAt = Number.POSITIVE_INFINITY;
19769
+ for (const sentAt of this.pendingHeartbeatAcks.values()) {
19770
+ oldestSentAt = Math.min(oldestSentAt, sentAt);
19771
+ }
19772
+ const nowMs = this.now();
19773
+ const oldestPendingAgeMs = nowMs - oldestSentAt;
19774
+ if (oldestPendingAgeMs < this.heartbeatAckTimeoutMs) {
19775
+ this.scheduleHeartbeatAckTimeoutCheck();
19776
+ return;
19777
+ }
19778
+ const socket = this.socket;
19779
+ if (socket === void 0 || !this.detachSocket(socket)) {
19780
+ return;
19781
+ }
19782
+ this.logger.warn("connector.websocket.heartbeat_ack_timeout", {
19783
+ pendingCount,
19784
+ oldestPendingAgeMs,
19785
+ timeoutMs: this.heartbeatAckTimeoutMs
19786
+ });
19787
+ this.closeSocketQuietly(socket, 1e3, "heartbeat ack timeout");
19788
+ this.hooks.onDisconnected?.({
19789
+ code: 1006,
19790
+ reason: "Heartbeat acknowledgement timed out",
19791
+ wasClean: false
19792
+ });
19793
+ if (this.started) {
19794
+ this.scheduleReconnect();
19422
19795
  }
19423
19796
  }
19424
19797
  flushOutboundQueue() {
@@ -19432,7 +19805,74 @@ var ConnectorClient = class {
19432
19805
  return;
19433
19806
  }
19434
19807
  this.outboundQueue.shift();
19808
+ this.persistOutboundQueue();
19809
+ }
19810
+ }
19811
+ recordOutboundQueueDepth() {
19812
+ this.maxObservedOutboundQueueDepth = Math.max(
19813
+ this.maxObservedOutboundQueueDepth,
19814
+ this.outboundQueue.length
19815
+ );
19816
+ }
19817
+ persistOutboundQueue() {
19818
+ if (this.outboundQueuePersistence === void 0) {
19819
+ return;
19820
+ }
19821
+ this.outboundQueueSaveChain = this.outboundQueueSaveChain.then(async () => {
19822
+ await this.ensureOutboundQueueLoaded();
19823
+ await this.outboundQueuePersistence?.save([...this.outboundQueue]);
19824
+ }).catch((error48) => {
19825
+ this.logger.warn("connector.outbound.persistence_save_failed", {
19826
+ reason: sanitizeErrorReason(error48)
19827
+ });
19828
+ });
19829
+ }
19830
+ async ensureOutboundQueueLoaded() {
19831
+ if (this.outboundQueueLoaded) {
19832
+ return;
19833
+ }
19834
+ if (this.outboundQueuePersistence === void 0) {
19835
+ this.outboundQueueLoaded = true;
19836
+ return;
19435
19837
  }
19838
+ if (this.outboundQueueLoadPromise !== void 0) {
19839
+ await this.outboundQueueLoadPromise;
19840
+ return;
19841
+ }
19842
+ this.outboundQueueLoadPromise = (async () => {
19843
+ try {
19844
+ const loadedFrames = await this.outboundQueuePersistence?.load();
19845
+ if (!loadedFrames || loadedFrames.length === 0) {
19846
+ return;
19847
+ }
19848
+ const existingIds = new Set(this.outboundQueue.map((item) => item.id));
19849
+ const validLoadedFrames = [];
19850
+ for (const candidate of loadedFrames) {
19851
+ const parsed = enqueueFrameSchema.safeParse(candidate);
19852
+ if (!parsed.success) {
19853
+ continue;
19854
+ }
19855
+ if (existingIds.has(parsed.data.id)) {
19856
+ continue;
19857
+ }
19858
+ validLoadedFrames.push(parsed.data);
19859
+ existingIds.add(parsed.data.id);
19860
+ }
19861
+ if (validLoadedFrames.length === 0) {
19862
+ return;
19863
+ }
19864
+ this.outboundQueue.unshift(...validLoadedFrames);
19865
+ this.recordOutboundQueueDepth();
19866
+ } catch (error48) {
19867
+ this.logger.warn("connector.outbound.persistence_load_failed", {
19868
+ reason: sanitizeErrorReason(error48)
19869
+ });
19870
+ } finally {
19871
+ this.outboundQueueLoaded = true;
19872
+ }
19873
+ })();
19874
+ await this.outboundQueueLoadPromise;
19875
+ this.flushOutboundQueue();
19436
19876
  }
19437
19877
  sendFrame(frame) {
19438
19878
  const socket = this.socket;
@@ -19466,6 +19906,10 @@ var ConnectorClient = class {
19466
19906
  this.handleHeartbeatFrame(frame);
19467
19907
  return;
19468
19908
  }
19909
+ if (frame.type === "heartbeat_ack") {
19910
+ this.handleHeartbeatAckFrame(frame);
19911
+ return;
19912
+ }
19469
19913
  if (frame.type === "deliver") {
19470
19914
  await this.handleDeliverFrame(frame);
19471
19915
  return;
@@ -19482,6 +19926,7 @@ var ConnectorClient = class {
19482
19926
  this.sendFrame(ackFrame);
19483
19927
  }
19484
19928
  async handleDeliverFrame(frame) {
19929
+ const startedAtMs = this.now();
19485
19930
  if (this.inboundDeliverHandler !== void 0) {
19486
19931
  try {
19487
19932
  const result = await this.inboundDeliverHandler(frame);
@@ -19505,6 +19950,7 @@ var ConnectorClient = class {
19505
19950
  )
19506
19951
  );
19507
19952
  }
19953
+ this.recordInboundDeliveryAckLatency(this.now() - startedAtMs);
19508
19954
  } catch (error48) {
19509
19955
  const ackFrame = {
19510
19956
  v: CONNECTOR_FRAME_VERSION,
@@ -19517,6 +19963,7 @@ var ConnectorClient = class {
19517
19963
  };
19518
19964
  this.sendFrame(ackFrame);
19519
19965
  this.hooks.onDeliverFailed?.(frame, error48);
19966
+ this.recordInboundDeliveryAckLatency(this.now() - startedAtMs);
19520
19967
  }
19521
19968
  return;
19522
19969
  }
@@ -19532,6 +19979,7 @@ var ConnectorClient = class {
19532
19979
  };
19533
19980
  this.sendFrame(ackFrame);
19534
19981
  this.hooks.onDeliverSucceeded?.(frame);
19982
+ this.recordInboundDeliveryAckLatency(this.now() - startedAtMs);
19535
19983
  } catch (error48) {
19536
19984
  const ackFrame = {
19537
19985
  v: CONNECTOR_FRAME_VERSION,
@@ -19544,8 +19992,19 @@ var ConnectorClient = class {
19544
19992
  };
19545
19993
  this.sendFrame(ackFrame);
19546
19994
  this.hooks.onDeliverFailed?.(frame, error48);
19995
+ this.recordInboundDeliveryAckLatency(this.now() - startedAtMs);
19547
19996
  }
19548
19997
  }
19998
+ recordInboundDeliveryAckLatency(durationMs) {
19999
+ const latencyMs = Math.max(0, Math.floor(durationMs));
20000
+ this.inboundAckLatencySampleCount += 1;
20001
+ this.inboundAckLatencyTotalMs += latencyMs;
20002
+ this.inboundAckLatencyMaxMs = Math.max(
20003
+ this.inboundAckLatencyMaxMs,
20004
+ latencyMs
20005
+ );
20006
+ this.inboundAckLatencyLastMs = latencyMs;
20007
+ }
19549
20008
  async deliverToLocalOpenclaw(frame) {
19550
20009
  const controller = new AbortController();
19551
20010
  const timeout = setTimeout(() => {
@@ -19553,6 +20012,9 @@ var ConnectorClient = class {
19553
20012
  }, this.openclawDeliverTimeoutMs);
19554
20013
  const headers = {
19555
20014
  "content-type": "application/json",
20015
+ "x-clawdentity-agent-did": frame.fromAgentDid,
20016
+ "x-clawdentity-to-agent-did": frame.toAgentDid,
20017
+ "x-clawdentity-verified": "true",
19556
20018
  "x-request-id": frame.id
19557
20019
  };
19558
20020
  if (this.openclawHookToken !== void 0) {
@@ -19568,7 +20030,7 @@ var ConnectorClient = class {
19568
20030
  if (!response.ok) {
19569
20031
  throw new LocalOpenclawDeliveryError({
19570
20032
  message: `Local OpenClaw hook rejected payload with status ${response.status}`,
19571
- retryable: response.status >= 500 || response.status === 404 || response.status === 429
20033
+ retryable: response.status === 401 || response.status === 403 || response.status >= 500 || response.status === 404 || response.status === 429
19572
20034
  });
19573
20035
  }
19574
20036
  } catch (error48) {
@@ -19631,7 +20093,7 @@ var ConnectorClient = class {
19631
20093
  return this.ulidFactory(this.now());
19632
20094
  }
19633
20095
  makeTimestamp() {
19634
- return new Date(this.now()).toISOString();
20096
+ return toIso(this.now());
19635
20097
  }
19636
20098
  };
19637
20099
 
@@ -19641,36 +20103,44 @@ import {
19641
20103
  mkdir as mkdir3,
19642
20104
  readFile as readFile3,
19643
20105
  rename as rename2,
20106
+ stat as stat2,
20107
+ unlink as unlink2,
19644
20108
  writeFile as writeFile3
19645
20109
  } from "fs/promises";
19646
20110
  import { dirname as dirname2, join as join4 } from "path";
19647
20111
  var INBOUND_INBOX_DIR_NAME = "inbound-inbox";
19648
20112
  var INBOUND_INBOX_INDEX_FILE_NAME = "index.json";
20113
+ var INBOUND_INBOX_INDEX_LOCK_FILE_NAME = "index.lock";
19649
20114
  var INBOUND_INBOX_EVENTS_FILE_NAME = "events.jsonl";
19650
- var INBOUND_INBOX_SCHEMA_VERSION = 1;
19651
- function nowIso2() {
19652
- return (/* @__PURE__ */ new Date()).toISOString();
19653
- }
20115
+ var INBOUND_INBOX_SCHEMA_VERSION = 2;
20116
+ var DEFAULT_INDEX_LOCK_TIMEOUT_MS = 5e3;
20117
+ var DEFAULT_INDEX_LOCK_STALE_MS = 3e4;
20118
+ var DEFAULT_INDEX_LOCK_RETRY_MS = 50;
19654
20119
  function isRecord5(value) {
19655
20120
  return typeof value === "object" && value !== null;
19656
20121
  }
20122
+ function parseOptionalNonEmptyString(value) {
20123
+ if (typeof value !== "string") {
20124
+ return void 0;
20125
+ }
20126
+ const trimmed = value.trim();
20127
+ return trimmed.length > 0 ? trimmed : void 0;
20128
+ }
19657
20129
  function parsePendingItem(value) {
19658
20130
  if (!isRecord5(value)) {
19659
20131
  return void 0;
19660
20132
  }
19661
- const id = typeof value.id === "string" ? value.id.trim() : "";
19662
- const requestId = typeof value.requestId === "string" ? value.requestId.trim() : "";
19663
- const fromAgentDid = typeof value.fromAgentDid === "string" ? value.fromAgentDid.trim() : "";
19664
- const toAgentDid = typeof value.toAgentDid === "string" ? value.toAgentDid.trim() : "";
19665
- const receivedAt = typeof value.receivedAt === "string" ? value.receivedAt.trim() : "";
19666
- const nextAttemptAt = typeof value.nextAttemptAt === "string" ? value.nextAttemptAt.trim() : "";
20133
+ const id = parseOptionalNonEmptyString(value.id) ?? "";
20134
+ const requestId = parseOptionalNonEmptyString(value.requestId) ?? "";
20135
+ const fromAgentDid = parseOptionalNonEmptyString(value.fromAgentDid) ?? "";
20136
+ const toAgentDid = parseOptionalNonEmptyString(value.toAgentDid) ?? "";
20137
+ const receivedAt = parseOptionalNonEmptyString(value.receivedAt) ?? "";
20138
+ const nextAttemptAt = parseOptionalNonEmptyString(value.nextAttemptAt) ?? "";
19667
20139
  const attemptCount = typeof value.attemptCount === "number" && Number.isInteger(value.attemptCount) ? value.attemptCount : NaN;
19668
20140
  const payloadBytes = typeof value.payloadBytes === "number" && Number.isInteger(value.payloadBytes) ? value.payloadBytes : NaN;
19669
20141
  if (id.length === 0 || requestId.length === 0 || fromAgentDid.length === 0 || toAgentDid.length === 0 || receivedAt.length === 0 || nextAttemptAt.length === 0 || !Number.isFinite(attemptCount) || attemptCount < 0 || !Number.isFinite(payloadBytes) || payloadBytes < 0) {
19670
20142
  return void 0;
19671
20143
  }
19672
- const lastError = typeof value.lastError === "string" ? value.lastError : void 0;
19673
- const lastAttemptAt = typeof value.lastAttemptAt === "string" ? value.lastAttemptAt : void 0;
19674
20144
  return {
19675
20145
  id,
19676
20146
  requestId,
@@ -19681,41 +20151,89 @@ function parsePendingItem(value) {
19681
20151
  receivedAt,
19682
20152
  nextAttemptAt,
19683
20153
  attemptCount,
19684
- lastError,
19685
- lastAttemptAt
20154
+ lastError: parseOptionalNonEmptyString(value.lastError),
20155
+ lastAttemptAt: parseOptionalNonEmptyString(value.lastAttemptAt),
20156
+ conversationId: parseOptionalNonEmptyString(value.conversationId),
20157
+ replyTo: parseOptionalNonEmptyString(value.replyTo)
20158
+ };
20159
+ }
20160
+ function parseDeadLetterItem(value) {
20161
+ const pending = parsePendingItem(value);
20162
+ if (!pending) {
20163
+ return void 0;
20164
+ }
20165
+ if (!isRecord5(value)) {
20166
+ return void 0;
20167
+ }
20168
+ const deadLetteredAt = parseOptionalNonEmptyString(value.deadLetteredAt) ?? "";
20169
+ const deadLetterReason = parseOptionalNonEmptyString(value.deadLetterReason) ?? "";
20170
+ if (deadLetteredAt.length === 0 || deadLetterReason.length === 0) {
20171
+ return void 0;
20172
+ }
20173
+ return {
20174
+ ...pending,
20175
+ deadLetteredAt,
20176
+ deadLetterReason
19686
20177
  };
19687
20178
  }
19688
20179
  function toDefaultIndexFile() {
19689
20180
  return {
19690
20181
  version: INBOUND_INBOX_SCHEMA_VERSION,
19691
20182
  pendingBytes: 0,
20183
+ deadLetterBytes: 0,
19692
20184
  pendingByRequestId: {},
19693
- updatedAt: nowIso2()
20185
+ deadLetterByRequestId: {},
20186
+ updatedAt: nowIso()
19694
20187
  };
19695
20188
  }
19696
20189
  function normalizeIndexFile(raw) {
19697
20190
  if (!isRecord5(raw)) {
19698
20191
  throw new Error("Inbound inbox index root must be an object");
19699
20192
  }
20193
+ if (raw.version !== INBOUND_INBOX_SCHEMA_VERSION) {
20194
+ throw new Error(
20195
+ `Inbound inbox index schema version ${String(raw.version)} is unsupported`
20196
+ );
20197
+ }
19700
20198
  const pendingByRequestIdRaw = raw.pendingByRequestId;
20199
+ const deadLetterByRequestIdRaw = raw.deadLetterByRequestId;
19701
20200
  if (!isRecord5(pendingByRequestIdRaw)) {
19702
20201
  throw new Error("Inbound inbox index pendingByRequestId must be an object");
19703
20202
  }
20203
+ if (!isRecord5(deadLetterByRequestIdRaw)) {
20204
+ throw new Error(
20205
+ "Inbound inbox index deadLetterByRequestId must be an object"
20206
+ );
20207
+ }
19704
20208
  const pendingByRequestId = {};
19705
20209
  let pendingBytes = 0;
19706
20210
  for (const [requestId, candidate] of Object.entries(pendingByRequestIdRaw)) {
19707
20211
  const entry = parsePendingItem(candidate);
19708
- if (entry === void 0 || entry.requestId !== requestId) {
20212
+ if (!entry || entry.requestId !== requestId) {
19709
20213
  continue;
19710
20214
  }
19711
20215
  pendingByRequestId[requestId] = entry;
19712
20216
  pendingBytes += entry.payloadBytes;
19713
20217
  }
20218
+ const deadLetterByRequestId = {};
20219
+ let deadLetterBytes = 0;
20220
+ for (const [requestId, candidate] of Object.entries(
20221
+ deadLetterByRequestIdRaw
20222
+ )) {
20223
+ const entry = parseDeadLetterItem(candidate);
20224
+ if (!entry || entry.requestId !== requestId) {
20225
+ continue;
20226
+ }
20227
+ deadLetterByRequestId[requestId] = entry;
20228
+ deadLetterBytes += entry.payloadBytes;
20229
+ }
19714
20230
  return {
19715
- version: typeof raw.version === "number" && Number.isFinite(raw.version) ? raw.version : INBOUND_INBOX_SCHEMA_VERSION,
19716
- pendingBytes,
20231
+ version: INBOUND_INBOX_SCHEMA_VERSION,
19717
20232
  pendingByRequestId,
19718
- updatedAt: typeof raw.updatedAt === "string" && raw.updatedAt.trim().length > 0 ? raw.updatedAt : nowIso2()
20233
+ deadLetterByRequestId,
20234
+ pendingBytes,
20235
+ deadLetterBytes,
20236
+ updatedAt: parseOptionalNonEmptyString(raw.updatedAt) ?? nowIso()
19719
20237
  };
19720
20238
  }
19721
20239
  function toComparableTimeMs(value) {
@@ -19727,11 +20245,14 @@ function toComparableTimeMs(value) {
19727
20245
  }
19728
20246
  var ConnectorInboundInbox = class {
19729
20247
  agentName;
20248
+ eventsMaxBytes;
20249
+ eventsMaxFiles;
19730
20250
  eventsPath;
20251
+ inboxDir;
19731
20252
  indexPath;
20253
+ indexLockPath;
19732
20254
  maxPendingBytes;
19733
20255
  maxPendingMessages;
19734
- inboxDir;
19735
20256
  writeChain = Promise.resolve();
19736
20257
  constructor(options) {
19737
20258
  this.agentName = options.agentName;
@@ -19742,15 +20263,20 @@ var ConnectorInboundInbox = class {
19742
20263
  INBOUND_INBOX_DIR_NAME
19743
20264
  );
19744
20265
  this.indexPath = join4(this.inboxDir, INBOUND_INBOX_INDEX_FILE_NAME);
20266
+ this.indexLockPath = join4(
20267
+ this.inboxDir,
20268
+ INBOUND_INBOX_INDEX_LOCK_FILE_NAME
20269
+ );
19745
20270
  this.eventsPath = join4(this.inboxDir, INBOUND_INBOX_EVENTS_FILE_NAME);
19746
20271
  this.maxPendingBytes = options.maxPendingBytes;
19747
20272
  this.maxPendingMessages = options.maxPendingMessages;
20273
+ this.eventsMaxBytes = Math.max(0, options.eventsMaxBytes);
20274
+ this.eventsMaxFiles = Math.max(0, options.eventsMaxFiles);
19748
20275
  }
19749
20276
  async enqueue(frame) {
19750
20277
  return await this.withWriteLock(async () => {
19751
20278
  const index = await this.loadIndex();
19752
- const existing = index.pendingByRequestId[frame.id];
19753
- if (existing !== void 0) {
20279
+ if (index.pendingByRequestId[frame.id] !== void 0 || index.deadLetterByRequestId[frame.id] !== void 0) {
19754
20280
  await this.appendEvent({
19755
20281
  type: "inbound_duplicate",
19756
20282
  requestId: frame.id
@@ -19789,13 +20315,15 @@ var ConnectorInboundInbox = class {
19789
20315
  toAgentDid: frame.toAgentDid,
19790
20316
  payload: frame.payload,
19791
20317
  payloadBytes,
19792
- receivedAt: nowIso2(),
19793
- nextAttemptAt: nowIso2(),
19794
- attemptCount: 0
20318
+ receivedAt: nowIso(),
20319
+ nextAttemptAt: nowIso(),
20320
+ attemptCount: 0,
20321
+ conversationId: parseOptionalNonEmptyString(frame.conversationId),
20322
+ replyTo: parseOptionalNonEmptyString(frame.replyTo)
19795
20323
  };
19796
20324
  index.pendingByRequestId[pendingItem.requestId] = pendingItem;
19797
20325
  index.pendingBytes += pendingItem.payloadBytes;
19798
- index.updatedAt = nowIso2();
20326
+ index.updatedAt = nowIso();
19799
20327
  await this.saveIndex(index);
19800
20328
  await this.appendEvent({
19801
20329
  type: "inbound_persisted",
@@ -19803,7 +20331,9 @@ var ConnectorInboundInbox = class {
19803
20331
  details: {
19804
20332
  payloadBytes,
19805
20333
  fromAgentDid: pendingItem.fromAgentDid,
19806
- toAgentDid: pendingItem.toAgentDid
20334
+ toAgentDid: pendingItem.toAgentDid,
20335
+ conversationId: pendingItem.conversationId,
20336
+ replyTo: pendingItem.replyTo
19807
20337
  }
19808
20338
  });
19809
20339
  return {
@@ -19834,7 +20364,7 @@ var ConnectorInboundInbox = class {
19834
20364
  }
19835
20365
  delete index.pendingByRequestId[requestId];
19836
20366
  index.pendingBytes = Math.max(0, index.pendingBytes - entry.payloadBytes);
19837
- index.updatedAt = nowIso2();
20367
+ index.updatedAt = nowIso();
19838
20368
  await this.saveIndex(index);
19839
20369
  await this.appendEvent({
19840
20370
  type: "replay_succeeded",
@@ -19843,17 +20373,44 @@ var ConnectorInboundInbox = class {
19843
20373
  });
19844
20374
  }
19845
20375
  async markReplayFailure(input) {
19846
- await this.withWriteLock(async () => {
20376
+ return await this.withWriteLock(async () => {
19847
20377
  const index = await this.loadIndex();
19848
20378
  const entry = index.pendingByRequestId[input.requestId];
19849
20379
  if (entry === void 0) {
19850
- return;
20380
+ return { movedToDeadLetter: false };
19851
20381
  }
19852
20382
  entry.attemptCount += 1;
19853
20383
  entry.lastError = input.errorMessage;
19854
- entry.lastAttemptAt = nowIso2();
20384
+ entry.lastAttemptAt = nowIso();
20385
+ const shouldMoveToDeadLetter = !input.retryable && entry.attemptCount >= Math.max(1, input.maxNonRetryableAttempts);
20386
+ if (shouldMoveToDeadLetter) {
20387
+ const deadLetterEntry = {
20388
+ ...entry,
20389
+ deadLetteredAt: nowIso(),
20390
+ deadLetterReason: input.errorMessage
20391
+ };
20392
+ delete index.pendingByRequestId[input.requestId];
20393
+ index.pendingBytes = Math.max(
20394
+ 0,
20395
+ index.pendingBytes - entry.payloadBytes
20396
+ );
20397
+ index.deadLetterByRequestId[input.requestId] = deadLetterEntry;
20398
+ index.deadLetterBytes += deadLetterEntry.payloadBytes;
20399
+ index.updatedAt = nowIso();
20400
+ await this.saveIndex(index);
20401
+ await this.appendEvent({
20402
+ type: "dead_letter_moved",
20403
+ requestId: input.requestId,
20404
+ details: {
20405
+ attemptCount: deadLetterEntry.attemptCount,
20406
+ retryable: input.retryable,
20407
+ errorMessage: input.errorMessage
20408
+ }
20409
+ });
20410
+ return { movedToDeadLetter: true };
20411
+ }
19855
20412
  entry.nextAttemptAt = input.nextAttemptAt;
19856
- index.updatedAt = nowIso2();
20413
+ index.updatedAt = nowIso();
19857
20414
  await this.saveIndex(index);
19858
20415
  await this.appendEvent({
19859
20416
  type: "replay_failed",
@@ -19861,62 +20418,184 @@ var ConnectorInboundInbox = class {
19861
20418
  details: {
19862
20419
  attemptCount: entry.attemptCount,
19863
20420
  nextAttemptAt: input.nextAttemptAt,
20421
+ retryable: input.retryable,
19864
20422
  errorMessage: input.errorMessage
19865
20423
  }
19866
20424
  });
20425
+ return { movedToDeadLetter: false };
19867
20426
  });
19868
20427
  }
19869
- async pruneDelivered() {
19870
- await this.withWriteLock(async () => {
19871
- const index = await this.loadIndex();
19872
- const beforeCount = Object.keys(index.pendingByRequestId).length;
19873
- if (beforeCount === 0) {
19874
- return;
20428
+ async listDeadLetter(input) {
20429
+ const index = await this.loadIndex();
20430
+ const entries = Object.values(index.deadLetterByRequestId).sort(
20431
+ (left, right) => {
20432
+ const leftDeadAt = toComparableTimeMs(left.deadLetteredAt);
20433
+ const rightDeadAt = toComparableTimeMs(right.deadLetteredAt);
20434
+ if (leftDeadAt !== rightDeadAt) {
20435
+ return leftDeadAt - rightDeadAt;
20436
+ }
20437
+ return toComparableTimeMs(left.receivedAt) - toComparableTimeMs(right.receivedAt);
19875
20438
  }
19876
- const after = {};
19877
- let pendingBytes = 0;
19878
- for (const [requestId, entry] of Object.entries(
19879
- index.pendingByRequestId
19880
- )) {
19881
- if (entry.attemptCount < 0) {
20439
+ );
20440
+ const limit = Math.max(1, input?.limit ?? (entries.length || 1));
20441
+ return entries.slice(0, limit);
20442
+ }
20443
+ async replayDeadLetter(input) {
20444
+ return await this.withWriteLock(async () => {
20445
+ const index = await this.loadIndex();
20446
+ const requestIds = input?.requestIds !== void 0 ? Array.from(
20447
+ new Set(
20448
+ input.requestIds.map((item) => item.trim()).filter((item) => item.length > 0)
20449
+ )
20450
+ ) : Object.keys(index.deadLetterByRequestId);
20451
+ let replayedCount = 0;
20452
+ for (const requestId of requestIds) {
20453
+ if (requestId.length === 0) {
19882
20454
  continue;
19883
20455
  }
19884
- after[requestId] = entry;
19885
- pendingBytes += entry.payloadBytes;
19886
- }
19887
- index.pendingByRequestId = after;
19888
- index.pendingBytes = pendingBytes;
19889
- index.updatedAt = nowIso2();
19890
- await this.saveIndex(index);
19891
- await this.appendEvent({
19892
- type: "inbox_pruned",
19893
- details: {
19894
- beforeCount,
19895
- afterCount: Object.keys(after).length
20456
+ const dead = index.deadLetterByRequestId[requestId];
20457
+ if (!dead) {
20458
+ continue;
19896
20459
  }
19897
- });
19898
- });
20460
+ delete index.deadLetterByRequestId[requestId];
20461
+ index.deadLetterBytes = Math.max(
20462
+ 0,
20463
+ index.deadLetterBytes - dead.payloadBytes
20464
+ );
20465
+ index.pendingByRequestId[requestId] = {
20466
+ ...dead,
20467
+ nextAttemptAt: nowIso(),
20468
+ lastError: dead.deadLetterReason
20469
+ };
20470
+ index.pendingBytes += dead.payloadBytes;
20471
+ replayedCount += 1;
20472
+ await this.appendEvent({
20473
+ type: "dead_letter_replayed",
20474
+ requestId,
20475
+ details: {
20476
+ deadLetteredAt: dead.deadLetteredAt,
20477
+ deadLetterReason: dead.deadLetterReason
20478
+ }
20479
+ });
20480
+ }
20481
+ if (replayedCount > 0) {
20482
+ index.updatedAt = nowIso();
20483
+ await this.saveIndex(index);
20484
+ }
20485
+ return { replayedCount };
20486
+ });
20487
+ }
20488
+ async purgeDeadLetter(input) {
20489
+ return await this.withWriteLock(async () => {
20490
+ const index = await this.loadIndex();
20491
+ const requestIds = input?.requestIds !== void 0 ? Array.from(
20492
+ new Set(
20493
+ input.requestIds.map((item) => item.trim()).filter((item) => item.length > 0)
20494
+ )
20495
+ ) : Object.keys(index.deadLetterByRequestId);
20496
+ let purgedCount = 0;
20497
+ for (const requestId of requestIds) {
20498
+ if (requestId.length === 0) {
20499
+ continue;
20500
+ }
20501
+ const dead = index.deadLetterByRequestId[requestId];
20502
+ if (!dead) {
20503
+ continue;
20504
+ }
20505
+ delete index.deadLetterByRequestId[requestId];
20506
+ index.deadLetterBytes = Math.max(
20507
+ 0,
20508
+ index.deadLetterBytes - dead.payloadBytes
20509
+ );
20510
+ purgedCount += 1;
20511
+ await this.appendEvent({
20512
+ type: "dead_letter_purged",
20513
+ requestId,
20514
+ details: {
20515
+ deadLetteredAt: dead.deadLetteredAt,
20516
+ deadLetterReason: dead.deadLetterReason
20517
+ }
20518
+ });
20519
+ }
20520
+ if (purgedCount > 0) {
20521
+ index.updatedAt = nowIso();
20522
+ await this.saveIndex(index);
20523
+ }
20524
+ return { purgedCount };
20525
+ });
20526
+ }
20527
+ async pruneDelivered() {
20528
+ await this.withWriteLock(async () => {
20529
+ const index = await this.loadIndex();
20530
+ const beforePendingCount = Object.keys(index.pendingByRequestId).length;
20531
+ const beforeDeadLetterCount = Object.keys(
20532
+ index.deadLetterByRequestId
20533
+ ).length;
20534
+ if (beforePendingCount === 0 && beforeDeadLetterCount === 0) {
20535
+ return;
20536
+ }
20537
+ const nextPending = {};
20538
+ let pendingBytes = 0;
20539
+ for (const [requestId, entry] of Object.entries(
20540
+ index.pendingByRequestId
20541
+ )) {
20542
+ if (entry.attemptCount < 0) {
20543
+ continue;
20544
+ }
20545
+ nextPending[requestId] = entry;
20546
+ pendingBytes += entry.payloadBytes;
20547
+ }
20548
+ const nextDead = {};
20549
+ let deadLetterBytes = 0;
20550
+ for (const [requestId, entry] of Object.entries(
20551
+ index.deadLetterByRequestId
20552
+ )) {
20553
+ if (entry.attemptCount < 0) {
20554
+ continue;
20555
+ }
20556
+ nextDead[requestId] = entry;
20557
+ deadLetterBytes += entry.payloadBytes;
20558
+ }
20559
+ index.pendingByRequestId = nextPending;
20560
+ index.pendingBytes = pendingBytes;
20561
+ index.deadLetterByRequestId = nextDead;
20562
+ index.deadLetterBytes = deadLetterBytes;
20563
+ index.updatedAt = nowIso();
20564
+ await this.saveIndex(index);
20565
+ await this.appendEvent({
20566
+ type: "inbox_pruned",
20567
+ details: {
20568
+ beforePendingCount,
20569
+ afterPendingCount: Object.keys(nextPending).length,
20570
+ beforeDeadLetterCount,
20571
+ afterDeadLetterCount: Object.keys(nextDead).length
20572
+ }
20573
+ });
20574
+ });
19899
20575
  }
19900
20576
  async getSnapshot() {
19901
20577
  const index = await this.loadIndex();
19902
- const entries = Object.values(index.pendingByRequestId);
19903
- if (entries.length === 0) {
19904
- return {
19905
- pendingCount: 0,
19906
- pendingBytes: index.pendingBytes
19907
- };
19908
- }
19909
- entries.sort((left, right) => {
19910
- return toComparableTimeMs(left.receivedAt) - toComparableTimeMs(right.receivedAt);
19911
- });
19912
- const nextAttemptAt = entries.map((entry) => entry.nextAttemptAt).sort(
20578
+ const pendingEntries = Object.values(index.pendingByRequestId).sort(
20579
+ (left, right) => toComparableTimeMs(left.receivedAt) - toComparableTimeMs(right.receivedAt)
20580
+ );
20581
+ const deadEntries = Object.values(index.deadLetterByRequestId).sort(
20582
+ (left, right) => toComparableTimeMs(left.deadLetteredAt) - toComparableTimeMs(right.deadLetteredAt)
20583
+ );
20584
+ const nextAttemptAt = pendingEntries.map((entry) => entry.nextAttemptAt).sort(
19913
20585
  (left, right) => toComparableTimeMs(left) - toComparableTimeMs(right)
19914
20586
  )[0];
19915
20587
  return {
19916
- pendingCount: entries.length,
19917
- pendingBytes: index.pendingBytes,
19918
- oldestPendingAt: entries[0]?.receivedAt,
19919
- nextAttemptAt
20588
+ pending: {
20589
+ pendingCount: pendingEntries.length,
20590
+ pendingBytes: index.pendingBytes,
20591
+ oldestPendingAt: pendingEntries[0]?.receivedAt,
20592
+ nextAttemptAt
20593
+ },
20594
+ deadLetter: {
20595
+ deadLetterCount: deadEntries.length,
20596
+ deadLetterBytes: index.deadLetterBytes,
20597
+ oldestDeadLetterAt: deadEntries[0]?.deadLetteredAt
20598
+ }
19920
20599
  };
19921
20600
  }
19922
20601
  async withWriteLock(fn) {
@@ -19926,12 +20605,67 @@ var ConnectorInboundInbox = class {
19926
20605
  release = resolve2;
19927
20606
  });
19928
20607
  await previous;
20608
+ const releaseFileLock = await this.acquireIndexFileLock();
19929
20609
  try {
19930
20610
  return await fn();
19931
20611
  } finally {
20612
+ await releaseFileLock();
19932
20613
  release?.();
19933
20614
  }
19934
20615
  }
20616
+ async acquireIndexFileLock() {
20617
+ const startedAt = nowUtcMs();
20618
+ await mkdir3(this.inboxDir, { recursive: true });
20619
+ while (true) {
20620
+ try {
20621
+ await writeFile3(
20622
+ this.indexLockPath,
20623
+ `${JSON.stringify({ pid: process.pid, createdAt: nowIso() })}
20624
+ `,
20625
+ {
20626
+ encoding: "utf8",
20627
+ flag: "wx"
20628
+ }
20629
+ );
20630
+ let released = false;
20631
+ return async () => {
20632
+ if (released) {
20633
+ return;
20634
+ }
20635
+ released = true;
20636
+ try {
20637
+ await unlink2(this.indexLockPath);
20638
+ } catch {
20639
+ }
20640
+ };
20641
+ } catch (error48) {
20642
+ const code = error48 && typeof error48 === "object" && "code" in error48 ? error48.code : void 0;
20643
+ if (code !== "EEXIST") {
20644
+ throw error48;
20645
+ }
20646
+ const lockStats = await this.readLockStats();
20647
+ if (lockStats !== void 0 && nowUtcMs() - lockStats.mtimeMs > DEFAULT_INDEX_LOCK_STALE_MS) {
20648
+ try {
20649
+ await unlink2(this.indexLockPath);
20650
+ } catch {
20651
+ }
20652
+ continue;
20653
+ }
20654
+ if (nowUtcMs() - startedAt >= DEFAULT_INDEX_LOCK_TIMEOUT_MS) {
20655
+ throw new Error("Timed out waiting for inbound inbox index lock");
20656
+ }
20657
+ await this.sleep(DEFAULT_INDEX_LOCK_RETRY_MS);
20658
+ }
20659
+ }
20660
+ }
20661
+ async readLockStats() {
20662
+ try {
20663
+ const lockStat = await stat2(this.indexLockPath);
20664
+ return { mtimeMs: lockStat.mtimeMs };
20665
+ } catch {
20666
+ return void 0;
20667
+ }
20668
+ }
19935
20669
  async loadIndex() {
19936
20670
  await mkdir3(this.inboxDir, { recursive: true });
19937
20671
  let raw;
@@ -19954,9 +20688,9 @@ var ConnectorInboundInbox = class {
19954
20688
  const payload = {
19955
20689
  ...index,
19956
20690
  version: INBOUND_INBOX_SCHEMA_VERSION,
19957
- updatedAt: nowIso2()
20691
+ updatedAt: nowIso()
19958
20692
  };
19959
- const tmpPath = `${this.indexPath}.tmp-${Date.now()}`;
20693
+ const tmpPath = `${this.indexPath}.tmp-${nowUtcMs()}`;
19960
20694
  await writeFile3(tmpPath, `${JSON.stringify(payload, null, 2)}
19961
20695
  `, "utf8");
19962
20696
  await rename2(tmpPath, this.indexPath);
@@ -19965,10 +20699,53 @@ var ConnectorInboundInbox = class {
19965
20699
  await mkdir3(dirname2(this.eventsPath), { recursive: true });
19966
20700
  await appendFile(
19967
20701
  this.eventsPath,
19968
- `${JSON.stringify({ ...event, at: nowIso2() })}
20702
+ `${JSON.stringify({ ...event, at: nowIso() })}
19969
20703
  `,
19970
20704
  "utf8"
19971
20705
  );
20706
+ await this.rotateEventsIfNeeded();
20707
+ }
20708
+ async rotateEventsIfNeeded() {
20709
+ if (this.eventsMaxBytes <= 0 || this.eventsMaxFiles <= 0) {
20710
+ return;
20711
+ }
20712
+ let currentSize;
20713
+ try {
20714
+ const current = await stat2(this.eventsPath);
20715
+ currentSize = current.size;
20716
+ } catch {
20717
+ return;
20718
+ }
20719
+ if (currentSize <= this.eventsMaxBytes) {
20720
+ return;
20721
+ }
20722
+ for (let index = this.eventsMaxFiles; index >= 1; index -= 1) {
20723
+ const fromPath = index === 1 ? this.eventsPath : `${this.eventsPath}.${index - 1}`;
20724
+ const toPath = `${this.eventsPath}.${index}`;
20725
+ const fromExists = await this.pathExists(fromPath);
20726
+ if (!fromExists) {
20727
+ continue;
20728
+ }
20729
+ const toExists = await this.pathExists(toPath);
20730
+ if (toExists) {
20731
+ await unlink2(toPath);
20732
+ }
20733
+ await rename2(fromPath, toPath);
20734
+ }
20735
+ await writeFile3(this.eventsPath, "", "utf8");
20736
+ }
20737
+ async pathExists(pathValue) {
20738
+ try {
20739
+ await stat2(pathValue);
20740
+ return true;
20741
+ } catch {
20742
+ return false;
20743
+ }
20744
+ }
20745
+ async sleep(durationMs) {
20746
+ await new Promise((resolve2) => {
20747
+ setTimeout(resolve2, durationMs);
20748
+ });
19972
20749
  }
19973
20750
  };
19974
20751
  function createConnectorInboundInbox(options) {
@@ -19981,10 +20758,13 @@ import { mkdir as mkdir4, readFile as readFile4, rename as rename3, writeFile as
19981
20758
  import {
19982
20759
  createServer
19983
20760
  } from "http";
19984
- import { dirname as dirname3, join as join5 } from "path";
20761
+ import { dirname as dirname3, isAbsolute, join as join5 } from "path";
19985
20762
  import { WebSocket as NodeWebSocket } from "ws";
19986
20763
  var REGISTRY_AUTH_FILENAME = "registry-auth.json";
20764
+ var OPENCLAW_RELAY_RUNTIME_FILE_NAME = "openclaw-relay.json";
19987
20765
  var AGENTS_DIR_NAME2 = "agents";
20766
+ var OUTBOUND_QUEUE_DIR_NAME = "outbound-queue";
20767
+ var OUTBOUND_QUEUE_FILENAME = "queue.json";
19988
20768
  var REFRESH_SINGLE_FLIGHT_PREFIX = "connector-runtime";
19989
20769
  var NONCE_SIZE = 16;
19990
20770
  var MAX_OUTBOUND_BODY_BYTES = 1024 * 1024;
@@ -20001,6 +20781,23 @@ function parseRequiredString(value, field) {
20001
20781
  }
20002
20782
  return value.trim();
20003
20783
  }
20784
+ function parseOptionalString(value) {
20785
+ if (typeof value !== "string") {
20786
+ return void 0;
20787
+ }
20788
+ const trimmed = value.trim();
20789
+ return trimmed.length > 0 ? trimmed : void 0;
20790
+ }
20791
+ function parseOptionalProxyOrigin(value) {
20792
+ if (typeof value !== "string" || value.trim().length === 0) {
20793
+ return void 0;
20794
+ }
20795
+ try {
20796
+ return new URL(value.trim()).origin;
20797
+ } catch {
20798
+ return void 0;
20799
+ }
20800
+ }
20004
20801
  function normalizeOutboundBaseUrl(baseUrlInput) {
20005
20802
  const raw = baseUrlInput?.trim() || DEFAULT_CONNECTOR_BASE_URL;
20006
20803
  let parsed;
@@ -20062,6 +20859,15 @@ function toOpenclawHookUrl2(baseUrl, hookPath) {
20062
20859
  const normalizedHookPath = hookPath.startsWith("/") ? hookPath.slice(1) : hookPath;
20063
20860
  return new URL(normalizedHookPath, normalizedBase).toString();
20064
20861
  }
20862
+ function toHttpOriginFromWebSocketUrl(value) {
20863
+ const normalized = new URL(value.toString());
20864
+ if (normalized.protocol === "wss:") {
20865
+ normalized.protocol = "https:";
20866
+ } else if (normalized.protocol === "ws:") {
20867
+ normalized.protocol = "http:";
20868
+ }
20869
+ return normalized.origin;
20870
+ }
20065
20871
  function parsePositiveIntEnv(key, fallback, minimum = 1) {
20066
20872
  const raw = process.env[key]?.trim();
20067
20873
  if (!raw) {
@@ -20080,10 +20886,12 @@ function sanitizeErrorReason2(error48) {
20080
20886
  return error48.message.trim().slice(0, 240) || "Unknown error";
20081
20887
  }
20082
20888
  var LocalOpenclawDeliveryError2 = class extends Error {
20889
+ code;
20083
20890
  retryable;
20084
20891
  constructor(input) {
20085
20892
  super(input.message);
20086
20893
  this.name = "LocalOpenclawDeliveryError";
20894
+ this.code = input.code;
20087
20895
  this.retryable = input.retryable;
20088
20896
  }
20089
20897
  };
@@ -20091,7 +20899,22 @@ function loadInboundReplayPolicy() {
20091
20899
  const retryBackoffFactor = Number.parseFloat(
20092
20900
  process.env.CONNECTOR_INBOUND_RETRY_BACKOFF_FACTOR ?? ""
20093
20901
  );
20902
+ const runtimeReplayRetryBackoffFactor = Number.parseFloat(
20903
+ process.env.CONNECTOR_RUNTIME_REPLAY_RETRY_BACKOFF_FACTOR ?? ""
20904
+ );
20094
20905
  return {
20906
+ deadLetterNonRetryableMaxAttempts: parsePositiveIntEnv(
20907
+ "CONNECTOR_INBOUND_DEAD_LETTER_NON_RETRYABLE_MAX_ATTEMPTS",
20908
+ DEFAULT_CONNECTOR_INBOUND_DEAD_LETTER_NON_RETRYABLE_MAX_ATTEMPTS
20909
+ ),
20910
+ eventsMaxBytes: parsePositiveIntEnv(
20911
+ "CONNECTOR_INBOUND_EVENTS_MAX_BYTES",
20912
+ DEFAULT_CONNECTOR_INBOUND_EVENTS_MAX_BYTES
20913
+ ),
20914
+ eventsMaxFiles: parsePositiveIntEnv(
20915
+ "CONNECTOR_INBOUND_EVENTS_MAX_FILES",
20916
+ DEFAULT_CONNECTOR_INBOUND_EVENTS_MAX_FILES
20917
+ ),
20095
20918
  inboxMaxMessages: parsePositiveIntEnv(
20096
20919
  "CONNECTOR_INBOUND_INBOX_MAX_MESSAGES",
20097
20920
  DEFAULT_CONNECTOR_INBOUND_INBOX_MAX_MESSAGES
@@ -20116,7 +20939,32 @@ function loadInboundReplayPolicy() {
20116
20939
  "CONNECTOR_INBOUND_RETRY_MAX_DELAY_MS",
20117
20940
  DEFAULT_CONNECTOR_INBOUND_RETRY_MAX_DELAY_MS
20118
20941
  ),
20119
- retryBackoffFactor: Number.isFinite(retryBackoffFactor) && retryBackoffFactor >= 1 ? retryBackoffFactor : DEFAULT_CONNECTOR_INBOUND_RETRY_BACKOFF_FACTOR
20942
+ retryBackoffFactor: Number.isFinite(retryBackoffFactor) && retryBackoffFactor >= 1 ? retryBackoffFactor : DEFAULT_CONNECTOR_INBOUND_RETRY_BACKOFF_FACTOR,
20943
+ runtimeReplayMaxAttempts: parsePositiveIntEnv(
20944
+ "CONNECTOR_RUNTIME_REPLAY_MAX_ATTEMPTS",
20945
+ DEFAULT_CONNECTOR_RUNTIME_REPLAY_DELIVER_MAX_ATTEMPTS
20946
+ ),
20947
+ runtimeReplayRetryInitialDelayMs: parsePositiveIntEnv(
20948
+ "CONNECTOR_RUNTIME_REPLAY_RETRY_INITIAL_DELAY_MS",
20949
+ DEFAULT_CONNECTOR_RUNTIME_REPLAY_DELIVER_RETRY_INITIAL_DELAY_MS
20950
+ ),
20951
+ runtimeReplayRetryMaxDelayMs: parsePositiveIntEnv(
20952
+ "CONNECTOR_RUNTIME_REPLAY_RETRY_MAX_DELAY_MS",
20953
+ DEFAULT_CONNECTOR_RUNTIME_REPLAY_DELIVER_RETRY_MAX_DELAY_MS
20954
+ ),
20955
+ runtimeReplayRetryBackoffFactor: Number.isFinite(runtimeReplayRetryBackoffFactor) && runtimeReplayRetryBackoffFactor >= 1 ? runtimeReplayRetryBackoffFactor : DEFAULT_CONNECTOR_RUNTIME_REPLAY_DELIVER_RETRY_BACKOFF_FACTOR
20956
+ };
20957
+ }
20958
+ function loadOpenclawProbePolicy() {
20959
+ return {
20960
+ intervalMs: parsePositiveIntEnv(
20961
+ "CONNECTOR_OPENCLAW_PROBE_INTERVAL_MS",
20962
+ DEFAULT_CONNECTOR_OPENCLAW_PROBE_INTERVAL_MS
20963
+ ),
20964
+ timeoutMs: parsePositiveIntEnv(
20965
+ "CONNECTOR_OPENCLAW_PROBE_TIMEOUT_MS",
20966
+ DEFAULT_CONNECTOR_OPENCLAW_PROBE_TIMEOUT_MS
20967
+ )
20120
20968
  };
20121
20969
  }
20122
20970
  function computeReplayDelayMs(input) {
@@ -20129,13 +20977,90 @@ function computeReplayDelayMs(input) {
20129
20977
  );
20130
20978
  return Math.max(1, delay);
20131
20979
  }
20980
+ function computeRuntimeReplayRetryDelayMs(input) {
20981
+ const exponent = Math.max(0, input.attemptCount - 1);
20982
+ const delay = Math.min(
20983
+ input.policy.runtimeReplayRetryMaxDelayMs,
20984
+ Math.floor(
20985
+ input.policy.runtimeReplayRetryInitialDelayMs * input.policy.runtimeReplayRetryBackoffFactor ** exponent
20986
+ )
20987
+ );
20988
+ return Math.max(1, delay);
20989
+ }
20990
+ async function waitWithAbort(input) {
20991
+ if (input.signal.aborted) {
20992
+ throw new LocalOpenclawDeliveryError2({
20993
+ code: "RUNTIME_STOPPING",
20994
+ message: "Connector runtime is stopping",
20995
+ retryable: false
20996
+ });
20997
+ }
20998
+ await new Promise((resolve2, reject) => {
20999
+ const timeoutHandle = setTimeout(() => {
21000
+ input.signal.removeEventListener("abort", onAbort);
21001
+ resolve2();
21002
+ }, input.delayMs);
21003
+ const onAbort = () => {
21004
+ clearTimeout(timeoutHandle);
21005
+ input.signal.removeEventListener("abort", onAbort);
21006
+ reject(
21007
+ new LocalOpenclawDeliveryError2({
21008
+ code: "RUNTIME_STOPPING",
21009
+ message: "Connector runtime is stopping",
21010
+ retryable: false
21011
+ })
21012
+ );
21013
+ };
21014
+ input.signal.addEventListener("abort", onAbort, { once: true });
21015
+ });
21016
+ }
21017
+ async function readOpenclawHookTokenFromRelayRuntimeConfig(input) {
21018
+ const runtimeConfigPath = join5(
21019
+ input.configDir,
21020
+ OPENCLAW_RELAY_RUNTIME_FILE_NAME
21021
+ );
21022
+ let raw;
21023
+ try {
21024
+ raw = await readFile4(runtimeConfigPath, "utf8");
21025
+ } catch (error48) {
21026
+ if (error48 && typeof error48 === "object" && "code" in error48 && error48.code === "ENOENT") {
21027
+ return void 0;
21028
+ }
21029
+ input.logger.warn("connector.runtime.openclaw_relay_config_read_failed", {
21030
+ runtimeConfigPath,
21031
+ reason: sanitizeErrorReason2(error48)
21032
+ });
21033
+ return void 0;
21034
+ }
21035
+ let parsed;
21036
+ try {
21037
+ parsed = JSON.parse(raw);
21038
+ } catch {
21039
+ input.logger.warn("connector.runtime.openclaw_relay_config_invalid_json", {
21040
+ runtimeConfigPath
21041
+ });
21042
+ return void 0;
21043
+ }
21044
+ if (!isRecord6(parsed)) {
21045
+ return void 0;
21046
+ }
21047
+ const tokenValue = parsed.openclawHookToken;
21048
+ if (typeof tokenValue !== "string") {
21049
+ return void 0;
21050
+ }
21051
+ const trimmed = tokenValue.trim();
21052
+ return trimmed.length > 0 ? trimmed : void 0;
21053
+ }
20132
21054
  async function deliverToOpenclawHook(input) {
20133
- const controller = new AbortController();
20134
- const timeoutHandle = setTimeout(() => {
20135
- controller.abort();
20136
- }, DEFAULT_OPENCLAW_DELIVER_TIMEOUT_MS);
21055
+ const timeoutSignal = AbortSignal.timeout(
21056
+ DEFAULT_OPENCLAW_DELIVER_TIMEOUT_MS
21057
+ );
21058
+ const signal = AbortSignal.any([input.shutdownSignal, timeoutSignal]);
20137
21059
  const headers = {
20138
21060
  "content-type": "application/json",
21061
+ "x-clawdentity-agent-did": input.fromAgentDid,
21062
+ "x-clawdentity-to-agent-did": input.toAgentDid,
21063
+ "x-clawdentity-verified": "true",
20139
21064
  "x-request-id": input.requestId
20140
21065
  };
20141
21066
  if (input.openclawHookToken !== void 0) {
@@ -20146,16 +21071,24 @@ async function deliverToOpenclawHook(input) {
20146
21071
  method: "POST",
20147
21072
  headers,
20148
21073
  body: JSON.stringify(input.payload),
20149
- signal: controller.signal
21074
+ signal
20150
21075
  });
20151
21076
  if (!response.ok) {
20152
21077
  throw new LocalOpenclawDeliveryError2({
20153
21078
  message: `Local OpenClaw hook rejected payload with status ${response.status}`,
20154
- retryable: response.status >= 500 || response.status === 404 || response.status === 429
21079
+ retryable: response.status === 401 || response.status === 403 || response.status >= 500 || response.status === 404 || response.status === 429,
21080
+ code: response.status === 401 || response.status === 403 ? "HOOK_AUTH_REJECTED" : void 0
20155
21081
  });
20156
21082
  }
20157
21083
  } catch (error48) {
20158
21084
  if (error48 instanceof Error && error48.name === "AbortError") {
21085
+ if (input.shutdownSignal.aborted) {
21086
+ throw new LocalOpenclawDeliveryError2({
21087
+ code: "RUNTIME_STOPPING",
21088
+ message: "Connector runtime is stopping",
21089
+ retryable: false
21090
+ });
21091
+ }
20159
21092
  throw new LocalOpenclawDeliveryError2({
20160
21093
  message: "Local OpenClaw hook request timed out",
20161
21094
  retryable: true
@@ -20168,8 +21101,6 @@ async function deliverToOpenclawHook(input) {
20168
21101
  message: sanitizeErrorReason2(error48),
20169
21102
  retryable: true
20170
21103
  });
20171
- } finally {
20172
- clearTimeout(timeoutHandle);
20173
21104
  }
20174
21105
  }
20175
21106
  function toInitialAuthBundle(credentials) {
@@ -20207,11 +21138,26 @@ function parseOutboundRelayRequest(payload) {
20207
21138
  expose: true
20208
21139
  });
20209
21140
  }
21141
+ const replyTo = parseOptionalString(payload.replyTo);
21142
+ if (replyTo !== void 0) {
21143
+ try {
21144
+ new URL(replyTo);
21145
+ } catch {
21146
+ throw new AppError({
21147
+ code: "CONNECTOR_OUTBOUND_INVALID_REQUEST",
21148
+ message: "Outbound relay replyTo must be a valid URL",
21149
+ status: 400,
21150
+ expose: true
21151
+ });
21152
+ }
21153
+ }
20210
21154
  return {
20211
21155
  peer: parseRequiredString(payload.peer, "peer"),
20212
21156
  peerDid: parseRequiredString(payload.peerDid, "peerDid"),
20213
21157
  peerProxyUrl: parseRequiredString(payload.peerProxyUrl, "peerProxyUrl"),
20214
- payload: payload.payload
21158
+ payload: payload.payload,
21159
+ conversationId: parseOptionalString(payload.conversationId),
21160
+ replyTo
20215
21161
  };
20216
21162
  }
20217
21163
  function createWebSocketFactory() {
@@ -20251,6 +21197,14 @@ function createWebSocketFactory() {
20251
21197
  });
20252
21198
  return;
20253
21199
  }
21200
+ if (type === "unexpected-response") {
21201
+ socket.on("unexpected-response", (_request, response) => {
21202
+ listener({
21203
+ status: response.statusCode
21204
+ });
21205
+ });
21206
+ return;
21207
+ }
20254
21208
  socket.on("error", (error48) => listener({ error: error48 }));
20255
21209
  }
20256
21210
  };
@@ -20263,7 +21217,7 @@ async function writeRegistryAuthAtomic(input) {
20263
21217
  input.agentName,
20264
21218
  REGISTRY_AUTH_FILENAME
20265
21219
  );
20266
- const tmpPath = `${targetPath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
21220
+ const tmpPath = `${targetPath}.tmp-${nowUtcMs()}-${Math.random().toString(16).slice(2)}`;
20267
21221
  await mkdir4(dirname3(targetPath), { recursive: true });
20268
21222
  await writeFile4(tmpPath, `${JSON.stringify(input.auth, null, 2)}
20269
21223
  `, "utf8");
@@ -20326,6 +21280,68 @@ async function readRegistryAuthFromDisk(input) {
20326
21280
  }
20327
21281
  return auth;
20328
21282
  }
21283
+ function resolveOutboundQueuePath(input) {
21284
+ return join5(
21285
+ input.configDir,
21286
+ AGENTS_DIR_NAME2,
21287
+ input.agentName,
21288
+ OUTBOUND_QUEUE_DIR_NAME,
21289
+ OUTBOUND_QUEUE_FILENAME
21290
+ );
21291
+ }
21292
+ function createOutboundQueuePersistence(input) {
21293
+ const queuePath = resolveOutboundQueuePath({
21294
+ configDir: input.configDir,
21295
+ agentName: input.agentName
21296
+ });
21297
+ const load = async () => {
21298
+ let raw;
21299
+ try {
21300
+ raw = await readFile4(queuePath, "utf8");
21301
+ } catch (error48) {
21302
+ if (error48 && typeof error48 === "object" && "code" in error48 && error48.code === "ENOENT") {
21303
+ return [];
21304
+ }
21305
+ input.logger.warn("connector.outbound.persistence_read_failed", {
21306
+ queuePath,
21307
+ reason: sanitizeErrorReason2(error48)
21308
+ });
21309
+ return [];
21310
+ }
21311
+ if (raw.trim().length === 0) {
21312
+ return [];
21313
+ }
21314
+ let parsed;
21315
+ try {
21316
+ parsed = JSON.parse(raw);
21317
+ } catch (error48) {
21318
+ input.logger.warn("connector.outbound.persistence_invalid_json", {
21319
+ queuePath,
21320
+ reason: sanitizeErrorReason2(error48)
21321
+ });
21322
+ return [];
21323
+ }
21324
+ if (!Array.isArray(parsed)) {
21325
+ return [];
21326
+ }
21327
+ const frames = [];
21328
+ for (const candidate of parsed) {
21329
+ const parsedFrame = enqueueFrameSchema.safeParse(candidate);
21330
+ if (parsedFrame.success) {
21331
+ frames.push(parsedFrame.data);
21332
+ }
21333
+ }
21334
+ return frames;
21335
+ };
21336
+ const save = async (frames) => {
21337
+ await mkdir4(dirname3(queuePath), { recursive: true });
21338
+ const tmpPath = `${queuePath}.tmp-${nowUtcMs()}-${Math.random().toString(16).slice(2)}`;
21339
+ await writeFile4(tmpPath, `${JSON.stringify(frames, null, 2)}
21340
+ `, "utf8");
21341
+ await rename3(tmpPath, queuePath);
21342
+ };
21343
+ return { load, save };
21344
+ }
20329
21345
  async function readRequestJson(req) {
20330
21346
  const chunks = [];
20331
21347
  let totalBytes = 0;
@@ -20357,6 +21373,19 @@ async function readRequestJson(req) {
20357
21373
  });
20358
21374
  }
20359
21375
  }
21376
+ function parseRequestIds(value) {
21377
+ if (value === void 0) {
21378
+ return void 0;
21379
+ }
21380
+ if (!Array.isArray(value)) {
21381
+ return [];
21382
+ }
21383
+ return Array.from(
21384
+ new Set(
21385
+ value.map((item) => typeof item === "string" ? item.trim() : "").filter((item) => item.length > 0)
21386
+ )
21387
+ );
21388
+ }
20360
21389
  function writeJson(res, status, payload) {
20361
21390
  res.statusCode = status;
20362
21391
  res.setHeader("content-type", "application/json; charset=utf-8");
@@ -20367,7 +21396,7 @@ function isRetryableRelayAuthError(error48) {
20367
21396
  return error48 instanceof AppError && error48.code === "OPENCLAW_RELAY_AGENT_AUTH_REJECTED" && error48.status === 401;
20368
21397
  }
20369
21398
  async function buildUpgradeHeaders(input) {
20370
- const timestamp = Math.floor(Date.now() / 1e3).toString();
21399
+ const timestamp = Math.floor(nowUtcMs() / 1e3).toString();
20371
21400
  const nonce = encodeBase64url(randomBytes2(NONCE_SIZE));
20372
21401
  const signed = await signHttpRequest({
20373
21402
  method: "GET",
@@ -20382,6 +21411,93 @@ async function buildUpgradeHeaders(input) {
20382
21411
  ...signed.headers
20383
21412
  };
20384
21413
  }
21414
+ async function loadTrustedReceiptTargets(input) {
21415
+ const trustedReceiptTargets = {
21416
+ origins: /* @__PURE__ */ new Set(),
21417
+ byAgentDid: /* @__PURE__ */ new Map()
21418
+ };
21419
+ const relayRuntimeConfigPath = join5(
21420
+ input.configDir,
21421
+ OPENCLAW_RELAY_RUNTIME_FILE_NAME
21422
+ );
21423
+ let relayRuntimeRaw;
21424
+ try {
21425
+ relayRuntimeRaw = await readFile4(relayRuntimeConfigPath, "utf8");
21426
+ } catch (error48) {
21427
+ if (error48 && typeof error48 === "object" && "code" in error48 && error48.code === "ENOENT") {
21428
+ return trustedReceiptTargets;
21429
+ }
21430
+ input.logger.warn("connector.delivery_receipt.runtime_config_read_failed", {
21431
+ relayRuntimeConfigPath,
21432
+ reason: sanitizeErrorReason2(error48)
21433
+ });
21434
+ return trustedReceiptTargets;
21435
+ }
21436
+ let relayRuntimeParsed;
21437
+ try {
21438
+ relayRuntimeParsed = JSON.parse(relayRuntimeRaw);
21439
+ } catch (error48) {
21440
+ input.logger.warn(
21441
+ "connector.delivery_receipt.runtime_config_invalid_json",
21442
+ {
21443
+ relayRuntimeConfigPath,
21444
+ reason: sanitizeErrorReason2(error48)
21445
+ }
21446
+ );
21447
+ return trustedReceiptTargets;
21448
+ }
21449
+ if (!isRecord6(relayRuntimeParsed)) {
21450
+ return trustedReceiptTargets;
21451
+ }
21452
+ const relayTransformPeersPathRaw = typeof relayRuntimeParsed.relayTransformPeersPath === "string" && relayRuntimeParsed.relayTransformPeersPath.trim().length > 0 ? relayRuntimeParsed.relayTransformPeersPath.trim() : void 0;
21453
+ if (!relayTransformPeersPathRaw) {
21454
+ return trustedReceiptTargets;
21455
+ }
21456
+ const relayTransformPeersPath = isAbsolute(relayTransformPeersPathRaw) ? relayTransformPeersPathRaw : join5(input.configDir, relayTransformPeersPathRaw);
21457
+ let relayTransformPeersRaw;
21458
+ try {
21459
+ relayTransformPeersRaw = await readFile4(relayTransformPeersPath, "utf8");
21460
+ } catch (error48) {
21461
+ input.logger.warn("connector.delivery_receipt.peers_snapshot_read_failed", {
21462
+ relayTransformPeersPath,
21463
+ reason: sanitizeErrorReason2(error48)
21464
+ });
21465
+ return trustedReceiptTargets;
21466
+ }
21467
+ let relayTransformPeersParsed;
21468
+ try {
21469
+ relayTransformPeersParsed = JSON.parse(relayTransformPeersRaw);
21470
+ } catch (error48) {
21471
+ input.logger.warn(
21472
+ "connector.delivery_receipt.peers_snapshot_invalid_json",
21473
+ {
21474
+ relayTransformPeersPath,
21475
+ reason: sanitizeErrorReason2(error48)
21476
+ }
21477
+ );
21478
+ return trustedReceiptTargets;
21479
+ }
21480
+ if (!isRecord6(relayTransformPeersParsed)) {
21481
+ return trustedReceiptTargets;
21482
+ }
21483
+ const peersValue = relayTransformPeersParsed.peers;
21484
+ if (!isRecord6(peersValue)) {
21485
+ return trustedReceiptTargets;
21486
+ }
21487
+ for (const peerValue of Object.values(peersValue)) {
21488
+ if (!isRecord6(peerValue)) {
21489
+ continue;
21490
+ }
21491
+ const agentDid = typeof peerValue.did === "string" && peerValue.did.trim().length > 0 ? peerValue.did.trim() : void 0;
21492
+ const origin = parseOptionalProxyOrigin(peerValue.proxyUrl);
21493
+ if (!agentDid || !origin) {
21494
+ continue;
21495
+ }
21496
+ trustedReceiptTargets.origins.add(origin);
21497
+ trustedReceiptTargets.byAgentDid.set(agentDid, origin);
21498
+ }
21499
+ return trustedReceiptTargets;
21500
+ }
20385
21501
  async function startConnectorRuntime(input) {
20386
21502
  const logger11 = input.logger ?? createLogger({ service: "connector", module: "runtime" });
20387
21503
  const fetchImpl = input.fetchImpl ?? fetch;
@@ -20408,9 +21524,12 @@ async function startConnectorRuntime(input) {
20408
21524
  };
20409
21525
  const refreshCurrentAuthIfNeeded = async () => {
20410
21526
  await syncAuthFromDisk();
20411
- if (!shouldRefreshAccessToken(currentAuth, Date.now())) {
21527
+ if (!shouldRefreshAccessToken(currentAuth, nowUtcMs())) {
20412
21528
  return;
20413
21529
  }
21530
+ await refreshCurrentAuth();
21531
+ };
21532
+ const refreshCurrentAuth = async () => {
20414
21533
  currentAuth = await refreshAgentAuthWithClawProof({
20415
21534
  registryUrl: input.registryUrl,
20416
21535
  ait: input.credentials.ait,
@@ -20427,23 +21546,47 @@ async function startConnectorRuntime(input) {
20427
21546
  await refreshCurrentAuthIfNeeded();
20428
21547
  const wsUrl = normalizeWebSocketUrl(input.proxyWebsocketUrl);
20429
21548
  const wsParsed = new URL(wsUrl);
21549
+ const defaultReceiptCallbackUrl = new URL(
21550
+ RELAY_DELIVERY_RECEIPTS_PATH.slice(1),
21551
+ `${toHttpOriginFromWebSocketUrl(wsParsed)}/`
21552
+ ).toString();
21553
+ const defaultReceiptCallbackOrigin = new URL(defaultReceiptCallbackUrl).origin;
20430
21554
  const openclawBaseUrl = resolveOpenclawBaseUrl(input.openclawBaseUrl);
21555
+ const openclawProbeUrl = openclawBaseUrl;
20431
21556
  const openclawHookPath = resolveOpenclawHookPath(input.openclawHookPath);
20432
- const openclawHookToken = resolveOpenclawHookToken(input.openclawHookToken);
21557
+ const explicitOpenclawHookToken = resolveOpenclawHookToken(
21558
+ input.openclawHookToken
21559
+ );
21560
+ const hasExplicitOpenclawHookToken = explicitOpenclawHookToken !== void 0;
21561
+ let currentOpenclawHookToken = explicitOpenclawHookToken;
20433
21562
  const openclawHookUrl = toOpenclawHookUrl2(openclawBaseUrl, openclawHookPath);
20434
21563
  const inboundReplayPolicy = loadInboundReplayPolicy();
21564
+ const openclawProbePolicy = loadOpenclawProbePolicy();
21565
+ const trustedReceiptTargets = await loadTrustedReceiptTargets({
21566
+ configDir: input.configDir,
21567
+ logger: logger11
21568
+ });
21569
+ trustedReceiptTargets.origins.add(defaultReceiptCallbackOrigin);
20435
21570
  const inboundInbox = createConnectorInboundInbox({
20436
21571
  configDir: input.configDir,
20437
21572
  agentName: input.agentName,
21573
+ eventsMaxBytes: inboundReplayPolicy.eventsMaxBytes,
21574
+ eventsMaxFiles: inboundReplayPolicy.eventsMaxFiles,
20438
21575
  maxPendingMessages: inboundReplayPolicy.inboxMaxMessages,
20439
21576
  maxPendingBytes: inboundReplayPolicy.inboxMaxBytes
20440
21577
  });
20441
21578
  const inboundReplayStatus = {
20442
21579
  replayerActive: false
20443
21580
  };
21581
+ const openclawGatewayProbeStatus = {
21582
+ reachable: true
21583
+ };
21584
+ let openclawProbeInFlight = false;
20444
21585
  let runtimeStopping = false;
20445
21586
  let replayInFlight = false;
20446
21587
  let replayIntervalHandle;
21588
+ let openclawProbeIntervalHandle;
21589
+ const runtimeShutdownController = new AbortController();
20447
21590
  const resolveUpgradeHeaders = async () => {
20448
21591
  await refreshCurrentAuthIfNeeded();
20449
21592
  return buildUpgradeHeaders({
@@ -20453,6 +21596,116 @@ async function startConnectorRuntime(input) {
20453
21596
  secretKey
20454
21597
  });
20455
21598
  };
21599
+ const syncOpenclawHookToken = async (reason) => {
21600
+ if (hasExplicitOpenclawHookToken) {
21601
+ return;
21602
+ }
21603
+ const diskToken = await readOpenclawHookTokenFromRelayRuntimeConfig({
21604
+ configDir: input.configDir,
21605
+ logger: logger11
21606
+ });
21607
+ const nextToken = diskToken;
21608
+ if (nextToken === currentOpenclawHookToken) {
21609
+ return;
21610
+ }
21611
+ currentOpenclawHookToken = nextToken;
21612
+ logger11.info("connector.runtime.openclaw_hook_token_synced", {
21613
+ reason,
21614
+ source: diskToken !== void 0 ? "openclaw-relay.json" : "unset",
21615
+ hasToken: currentOpenclawHookToken !== void 0
21616
+ });
21617
+ };
21618
+ const probeOpenclawGateway = async () => {
21619
+ if (runtimeStopping || openclawProbeInFlight) {
21620
+ return;
21621
+ }
21622
+ openclawProbeInFlight = true;
21623
+ const checkedAt = nowIso();
21624
+ try {
21625
+ const timeoutSignal = AbortSignal.timeout(openclawProbePolicy.timeoutMs);
21626
+ const signal = AbortSignal.any([
21627
+ runtimeShutdownController.signal,
21628
+ timeoutSignal
21629
+ ]);
21630
+ await fetchImpl(openclawProbeUrl, {
21631
+ method: "GET",
21632
+ signal
21633
+ });
21634
+ openclawGatewayProbeStatus.reachable = true;
21635
+ openclawGatewayProbeStatus.lastCheckedAt = checkedAt;
21636
+ openclawGatewayProbeStatus.lastSuccessAt = checkedAt;
21637
+ openclawGatewayProbeStatus.lastFailureReason = void 0;
21638
+ } catch (error48) {
21639
+ if (runtimeShutdownController.signal.aborted) {
21640
+ return;
21641
+ }
21642
+ openclawGatewayProbeStatus.reachable = false;
21643
+ openclawGatewayProbeStatus.lastCheckedAt = checkedAt;
21644
+ openclawGatewayProbeStatus.lastFailureReason = sanitizeErrorReason2(error48);
21645
+ } finally {
21646
+ openclawProbeInFlight = false;
21647
+ }
21648
+ };
21649
+ const deliverToOpenclawHookWithRetry = async (inputReplay) => {
21650
+ let attempt = 1;
21651
+ while (true) {
21652
+ try {
21653
+ await deliverToOpenclawHook({
21654
+ fetchImpl,
21655
+ fromAgentDid: inputReplay.fromAgentDid,
21656
+ openclawHookUrl,
21657
+ openclawHookToken: currentOpenclawHookToken,
21658
+ payload: inputReplay.payload,
21659
+ requestId: inputReplay.requestId,
21660
+ shutdownSignal: runtimeShutdownController.signal,
21661
+ toAgentDid: inputReplay.toAgentDid
21662
+ });
21663
+ return;
21664
+ } catch (error48) {
21665
+ if (error48 instanceof LocalOpenclawDeliveryError2 && error48.code === "RUNTIME_STOPPING") {
21666
+ throw error48;
21667
+ }
21668
+ const retryable = error48 instanceof LocalOpenclawDeliveryError2 ? error48.retryable : true;
21669
+ const authRejected = error48 instanceof LocalOpenclawDeliveryError2 && error48.code === "HOOK_AUTH_REJECTED";
21670
+ if (authRejected) {
21671
+ const previousToken = currentOpenclawHookToken;
21672
+ await syncOpenclawHookToken("auth_rejected");
21673
+ const tokenChanged = currentOpenclawHookToken !== previousToken;
21674
+ const attemptsRemaining2 = attempt < inboundReplayPolicy.runtimeReplayMaxAttempts;
21675
+ if (tokenChanged && !runtimeStopping && attemptsRemaining2) {
21676
+ logger11.warn(
21677
+ "connector.inbound.replay_hook_auth_rejected_retrying",
21678
+ {
21679
+ requestId: inputReplay.requestId,
21680
+ attempt
21681
+ }
21682
+ );
21683
+ attempt += 1;
21684
+ continue;
21685
+ }
21686
+ }
21687
+ const attemptsRemaining = attempt < inboundReplayPolicy.runtimeReplayMaxAttempts;
21688
+ if (!retryable || !attemptsRemaining || runtimeStopping) {
21689
+ throw error48;
21690
+ }
21691
+ const retryDelayMs = computeRuntimeReplayRetryDelayMs({
21692
+ attemptCount: attempt,
21693
+ policy: inboundReplayPolicy
21694
+ });
21695
+ logger11.warn("connector.inbound.replay_retry_scheduled", {
21696
+ requestId: inputReplay.requestId,
21697
+ attempt,
21698
+ retryDelayMs,
21699
+ reason: sanitizeErrorReason2(error48)
21700
+ });
21701
+ await waitWithAbort({
21702
+ delayMs: retryDelayMs,
21703
+ signal: runtimeShutdownController.signal
21704
+ });
21705
+ attempt += 1;
21706
+ }
21707
+ }
21708
+ };
20456
21709
  const replayPendingInboundMessages = async () => {
20457
21710
  if (runtimeStopping || replayInFlight) {
20458
21711
  return;
@@ -20461,64 +21714,141 @@ async function startConnectorRuntime(input) {
20461
21714
  inboundReplayStatus.replayerActive = true;
20462
21715
  try {
20463
21716
  const dueItems = await inboundInbox.listDuePending({
20464
- nowMs: Date.now(),
21717
+ nowMs: nowUtcMs(),
20465
21718
  limit: inboundReplayPolicy.batchSize
20466
21719
  });
21720
+ if (dueItems.length === 0) {
21721
+ return;
21722
+ }
21723
+ await syncOpenclawHookToken("batch");
21724
+ if (!openclawGatewayProbeStatus.reachable) {
21725
+ logger11.info("connector.inbound.replay_skipped_gateway_unreachable", {
21726
+ pendingCount: dueItems.length,
21727
+ openclawBaseUrl: openclawProbeUrl,
21728
+ lastFailureReason: openclawGatewayProbeStatus.lastFailureReason
21729
+ });
21730
+ return;
21731
+ }
21732
+ const laneByKey = /* @__PURE__ */ new Map();
20467
21733
  for (const pending of dueItems) {
20468
- inboundReplayStatus.lastAttemptAt = (/* @__PURE__ */ new Date()).toISOString();
20469
- try {
20470
- await deliverToOpenclawHook({
20471
- fetchImpl,
20472
- openclawHookUrl,
20473
- openclawHookToken,
20474
- requestId: pending.requestId,
20475
- payload: pending.payload
20476
- });
20477
- await inboundInbox.markDelivered(pending.requestId);
20478
- inboundReplayStatus.lastReplayAt = (/* @__PURE__ */ new Date()).toISOString();
20479
- inboundReplayStatus.lastReplayError = void 0;
20480
- inboundReplayStatus.lastAttemptStatus = "ok";
20481
- logger11.info("connector.inbound.replay_succeeded", {
20482
- requestId: pending.requestId,
20483
- attemptCount: pending.attemptCount + 1
20484
- });
20485
- } catch (error48) {
20486
- const reason = sanitizeErrorReason2(error48);
20487
- const retryable = error48 instanceof LocalOpenclawDeliveryError2 ? error48.retryable : true;
20488
- const nextAttemptAt = new Date(
20489
- Date.now() + computeReplayDelayMs({
20490
- attemptCount: pending.attemptCount + 1,
20491
- policy: inboundReplayPolicy
20492
- }) * (retryable ? 1 : 10)
20493
- ).toISOString();
20494
- await inboundInbox.markReplayFailure({
20495
- requestId: pending.requestId,
20496
- errorMessage: reason,
20497
- nextAttemptAt
20498
- });
20499
- inboundReplayStatus.lastReplayError = reason;
20500
- inboundReplayStatus.lastAttemptStatus = "failed";
20501
- logger11.warn("connector.inbound.replay_failed", {
20502
- requestId: pending.requestId,
20503
- attemptCount: pending.attemptCount + 1,
20504
- retryable,
20505
- nextAttemptAt,
20506
- reason
20507
- });
21734
+ const laneKey = pending.conversationId !== void 0 ? `conversation:${pending.conversationId}` : "legacy-best-effort";
21735
+ const lane = laneByKey.get(laneKey);
21736
+ if (lane) {
21737
+ lane.push(pending);
21738
+ } else {
21739
+ laneByKey.set(laneKey, [pending]);
20508
21740
  }
20509
21741
  }
21742
+ await Promise.all(
21743
+ Array.from(laneByKey.values()).map(async (laneItems) => {
21744
+ for (const pending of laneItems) {
21745
+ inboundReplayStatus.lastAttemptAt = nowIso();
21746
+ try {
21747
+ await deliverToOpenclawHookWithRetry({
21748
+ fromAgentDid: pending.fromAgentDid,
21749
+ requestId: pending.requestId,
21750
+ payload: pending.payload,
21751
+ toAgentDid: pending.toAgentDid
21752
+ });
21753
+ await inboundInbox.markDelivered(pending.requestId);
21754
+ inboundReplayStatus.lastReplayAt = nowIso();
21755
+ inboundReplayStatus.lastReplayError = void 0;
21756
+ inboundReplayStatus.lastAttemptStatus = "ok";
21757
+ logger11.info("connector.inbound.replay_succeeded", {
21758
+ requestId: pending.requestId,
21759
+ attemptCount: pending.attemptCount + 1,
21760
+ conversationId: pending.conversationId
21761
+ });
21762
+ if (pending.replyTo) {
21763
+ try {
21764
+ await postDeliveryReceipt({
21765
+ requestId: pending.requestId,
21766
+ senderAgentDid: pending.fromAgentDid,
21767
+ recipientAgentDid: pending.toAgentDid,
21768
+ replyTo: pending.replyTo,
21769
+ status: "processed_by_openclaw"
21770
+ });
21771
+ } catch (error48) {
21772
+ logger11.warn("connector.inbound.delivery_receipt_failed", {
21773
+ requestId: pending.requestId,
21774
+ reason: sanitizeErrorReason2(error48),
21775
+ status: "processed_by_openclaw"
21776
+ });
21777
+ }
21778
+ }
21779
+ } catch (error48) {
21780
+ if (error48 instanceof LocalOpenclawDeliveryError2 && error48.code === "RUNTIME_STOPPING") {
21781
+ logger11.info("connector.inbound.replay_stopped", {
21782
+ requestId: pending.requestId
21783
+ });
21784
+ return;
21785
+ }
21786
+ const reason = sanitizeErrorReason2(error48);
21787
+ const retryable = error48 instanceof LocalOpenclawDeliveryError2 ? error48.retryable : true;
21788
+ const nextAttemptAt = toIso(
21789
+ nowUtcMs() + computeReplayDelayMs({
21790
+ attemptCount: pending.attemptCount + 1,
21791
+ policy: inboundReplayPolicy
21792
+ }) * (retryable ? 1 : 10)
21793
+ );
21794
+ const markResult = await inboundInbox.markReplayFailure({
21795
+ requestId: pending.requestId,
21796
+ errorMessage: reason,
21797
+ nextAttemptAt,
21798
+ retryable,
21799
+ maxNonRetryableAttempts: inboundReplayPolicy.deadLetterNonRetryableMaxAttempts
21800
+ });
21801
+ inboundReplayStatus.lastReplayError = reason;
21802
+ inboundReplayStatus.lastAttemptStatus = "failed";
21803
+ logger11.warn("connector.inbound.replay_failed", {
21804
+ requestId: pending.requestId,
21805
+ attemptCount: pending.attemptCount + 1,
21806
+ retryable,
21807
+ nextAttemptAt,
21808
+ movedToDeadLetter: markResult.movedToDeadLetter,
21809
+ reason
21810
+ });
21811
+ if (markResult.movedToDeadLetter && pending.replyTo) {
21812
+ try {
21813
+ await postDeliveryReceipt({
21814
+ requestId: pending.requestId,
21815
+ senderAgentDid: pending.fromAgentDid,
21816
+ recipientAgentDid: pending.toAgentDid,
21817
+ replyTo: pending.replyTo,
21818
+ status: "dead_lettered",
21819
+ reason
21820
+ });
21821
+ } catch (receiptError) {
21822
+ logger11.warn("connector.inbound.delivery_receipt_failed", {
21823
+ requestId: pending.requestId,
21824
+ reason: sanitizeErrorReason2(receiptError),
21825
+ status: "dead_lettered"
21826
+ });
21827
+ }
21828
+ }
21829
+ }
21830
+ }
21831
+ })
21832
+ );
20510
21833
  } finally {
20511
21834
  replayInFlight = false;
20512
21835
  inboundReplayStatus.replayerActive = false;
20513
21836
  }
20514
21837
  };
20515
21838
  const readInboundReplayView = async () => {
20516
- const pending = await inboundInbox.getSnapshot();
21839
+ const snapshot = await inboundInbox.getSnapshot();
20517
21840
  return {
20518
- pending,
21841
+ snapshot,
20519
21842
  replayerActive: inboundReplayStatus.replayerActive || replayInFlight,
20520
21843
  lastReplayAt: inboundReplayStatus.lastReplayAt,
20521
21844
  lastReplayError: inboundReplayStatus.lastReplayError,
21845
+ openclawGateway: {
21846
+ url: openclawProbeUrl,
21847
+ reachable: openclawGatewayProbeStatus.reachable,
21848
+ lastCheckedAt: openclawGatewayProbeStatus.lastCheckedAt,
21849
+ lastSuccessAt: openclawGatewayProbeStatus.lastSuccessAt,
21850
+ lastFailureReason: openclawGatewayProbeStatus.lastFailureReason
21851
+ },
20522
21852
  openclawHook: {
20523
21853
  url: openclawHookUrl,
20524
21854
  lastAttemptAt: inboundReplayStatus.lastAttemptAt,
@@ -20526,14 +21856,39 @@ async function startConnectorRuntime(input) {
20526
21856
  }
20527
21857
  };
20528
21858
  };
21859
+ const outboundQueuePersistence = createOutboundQueuePersistence({
21860
+ configDir: input.configDir,
21861
+ agentName: input.agentName,
21862
+ logger: logger11
21863
+ });
20529
21864
  const connectorClient = new ConnectorClient({
20530
21865
  connectorUrl: wsParsed.toString(),
20531
21866
  connectionHeadersProvider: resolveUpgradeHeaders,
20532
21867
  openclawBaseUrl,
20533
21868
  openclawHookPath,
20534
- openclawHookToken,
21869
+ openclawHookToken: currentOpenclawHookToken,
20535
21870
  fetchImpl,
20536
21871
  logger: logger11,
21872
+ hooks: {
21873
+ onAuthUpgradeRejected: async ({ status, immediateRetry }) => {
21874
+ logger11.warn("connector.websocket.auth_upgrade_rejected", {
21875
+ status,
21876
+ immediateRetry
21877
+ });
21878
+ await syncAuthFromDisk();
21879
+ try {
21880
+ await refreshCurrentAuth();
21881
+ } catch (error48) {
21882
+ logger11.warn(
21883
+ "connector.runtime.registry_auth_refresh_on_ws_upgrade_reject_failed",
21884
+ {
21885
+ reason: sanitizeErrorReason2(error48)
21886
+ }
21887
+ );
21888
+ }
21889
+ }
21890
+ },
21891
+ outboundQueuePersistence,
20537
21892
  inboundDeliverHandler: async (frame) => {
20538
21893
  const persisted = await inboundInbox.enqueue(frame);
20539
21894
  if (!persisted.accepted) {
@@ -20560,30 +21915,136 @@ async function startConnectorRuntime(input) {
20560
21915
  const outboundBaseUrl = normalizeOutboundBaseUrl(input.outboundBaseUrl);
20561
21916
  const outboundPath = normalizeOutboundPath(input.outboundPath);
20562
21917
  const statusPath = DEFAULT_CONNECTOR_STATUS_PATH;
21918
+ const deadLetterPath = "/v1/inbound/dead-letter";
21919
+ const deadLetterReplayPath = "/v1/inbound/dead-letter/replay";
21920
+ const deadLetterPurgePath = "/v1/inbound/dead-letter/purge";
20563
21921
  const outboundUrl = new URL(outboundPath, outboundBaseUrl).toString();
20564
21922
  const relayToPeer = async (request) => {
20565
21923
  await syncAuthFromDisk();
20566
- const peerUrl = new URL(request.peerProxyUrl);
20567
- const body = JSON.stringify(request.payload ?? {});
20568
- const refreshKey = `${REFRESH_SINGLE_FLIGHT_PREFIX}:${input.configDir}:${input.agentName}`;
20569
- const performRelay = async (auth) => {
20570
- const unixSeconds = Math.floor(Date.now() / 1e3).toString();
21924
+ const peerUrl = new URL(request.peerProxyUrl);
21925
+ trustedReceiptTargets.origins.add(peerUrl.origin);
21926
+ trustedReceiptTargets.byAgentDid.set(request.peerDid, peerUrl.origin);
21927
+ const body = JSON.stringify(request.payload ?? {});
21928
+ const refreshKey = `${REFRESH_SINGLE_FLIGHT_PREFIX}:${input.configDir}:${input.agentName}`;
21929
+ const performRelay = async (auth) => {
21930
+ const replyTo = request.replyTo ?? defaultReceiptCallbackUrl;
21931
+ const unixSeconds = Math.floor(nowUtcMs() / 1e3).toString();
21932
+ const nonce = encodeBase64url(randomBytes2(NONCE_SIZE));
21933
+ const signed = await signHttpRequest({
21934
+ method: "POST",
21935
+ pathWithQuery: toPathWithQuery2(peerUrl),
21936
+ timestamp: unixSeconds,
21937
+ nonce,
21938
+ body: new TextEncoder().encode(body),
21939
+ secretKey
21940
+ });
21941
+ const response = await fetchImpl(peerUrl.toString(), {
21942
+ method: "POST",
21943
+ headers: {
21944
+ Authorization: `Claw ${input.credentials.ait}`,
21945
+ "Content-Type": "application/json",
21946
+ [AGENT_ACCESS_HEADER]: auth.accessToken,
21947
+ [RELAY_RECIPIENT_AGENT_DID_HEADER]: request.peerDid,
21948
+ ...request.conversationId ? { [RELAY_CONVERSATION_ID_HEADER]: request.conversationId } : {},
21949
+ [RELAY_DELIVERY_RECEIPT_URL_HEADER]: replyTo,
21950
+ ...signed.headers
21951
+ },
21952
+ body
21953
+ });
21954
+ if (!response.ok) {
21955
+ if (response.status === 401) {
21956
+ throw new AppError({
21957
+ code: "OPENCLAW_RELAY_AGENT_AUTH_REJECTED",
21958
+ message: "Peer relay rejected agent auth credentials",
21959
+ status: 401,
21960
+ expose: true
21961
+ });
21962
+ }
21963
+ throw new AppError({
21964
+ code: "CONNECTOR_OUTBOUND_DELIVERY_FAILED",
21965
+ message: "Peer relay request failed",
21966
+ status: 502
21967
+ });
21968
+ }
21969
+ };
21970
+ await executeWithAgentAuthRefreshRetry({
21971
+ key: refreshKey,
21972
+ shouldRetry: isRetryableRelayAuthError,
21973
+ getAuth: async () => {
21974
+ await syncAuthFromDisk();
21975
+ return currentAuth;
21976
+ },
21977
+ persistAuth: async (nextAuth) => {
21978
+ currentAuth = nextAuth;
21979
+ await writeRegistryAuthAtomic({
21980
+ configDir: input.configDir,
21981
+ agentName: input.agentName,
21982
+ auth: nextAuth
21983
+ });
21984
+ },
21985
+ refreshAuth: async (auth) => refreshAgentAuthWithClawProof({
21986
+ registryUrl: input.registryUrl,
21987
+ ait: input.credentials.ait,
21988
+ secretKey,
21989
+ refreshToken: auth.refreshToken,
21990
+ fetchImpl
21991
+ }),
21992
+ perform: performRelay
21993
+ });
21994
+ };
21995
+ const postDeliveryReceipt = async (inputReceipt) => {
21996
+ await syncAuthFromDisk();
21997
+ const receiptUrl = new URL(inputReceipt.replyTo);
21998
+ if (receiptUrl.pathname !== RELAY_DELIVERY_RECEIPTS_PATH) {
21999
+ throw new AppError({
22000
+ code: "CONNECTOR_DELIVERY_RECEIPT_INVALID_TARGET",
22001
+ message: "Delivery receipt callback target is invalid",
22002
+ status: 400
22003
+ });
22004
+ }
22005
+ const expectedSenderOrigin = trustedReceiptTargets.byAgentDid.get(
22006
+ inputReceipt.senderAgentDid
22007
+ );
22008
+ if (expectedSenderOrigin !== void 0 && receiptUrl.origin !== expectedSenderOrigin) {
22009
+ throw new AppError({
22010
+ code: "CONNECTOR_DELIVERY_RECEIPT_UNTRUSTED_TARGET",
22011
+ message: "Delivery receipt callback target is untrusted",
22012
+ status: 400
22013
+ });
22014
+ }
22015
+ if (expectedSenderOrigin === void 0 && !trustedReceiptTargets.origins.has(receiptUrl.origin)) {
22016
+ throw new AppError({
22017
+ code: "CONNECTOR_DELIVERY_RECEIPT_UNTRUSTED_TARGET",
22018
+ message: "Delivery receipt callback target is untrusted",
22019
+ status: 400
22020
+ });
22021
+ }
22022
+ const body = JSON.stringify({
22023
+ requestId: inputReceipt.requestId,
22024
+ senderAgentDid: inputReceipt.senderAgentDid,
22025
+ recipientAgentDid: inputReceipt.recipientAgentDid,
22026
+ status: inputReceipt.status,
22027
+ reason: inputReceipt.reason,
22028
+ processedAt: nowIso()
22029
+ });
22030
+ const refreshKey = `${REFRESH_SINGLE_FLIGHT_PREFIX}:${input.configDir}:${input.agentName}:delivery-receipt`;
22031
+ const performReceipt = async (auth) => {
22032
+ const unixSeconds = Math.floor(nowUtcMs() / 1e3).toString();
20571
22033
  const nonce = encodeBase64url(randomBytes2(NONCE_SIZE));
20572
22034
  const signed = await signHttpRequest({
20573
22035
  method: "POST",
20574
- pathWithQuery: toPathWithQuery2(peerUrl),
22036
+ pathWithQuery: toPathWithQuery2(receiptUrl),
20575
22037
  timestamp: unixSeconds,
20576
22038
  nonce,
20577
22039
  body: new TextEncoder().encode(body),
20578
22040
  secretKey
20579
22041
  });
20580
- const response = await fetchImpl(peerUrl.toString(), {
22042
+ const response = await fetchImpl(receiptUrl.toString(), {
20581
22043
  method: "POST",
20582
22044
  headers: {
20583
22045
  Authorization: `Claw ${input.credentials.ait}`,
20584
22046
  "Content-Type": "application/json",
20585
22047
  [AGENT_ACCESS_HEADER]: auth.accessToken,
20586
- [RELAY_RECIPIENT_AGENT_DID_HEADER]: request.peerDid,
20587
22048
  ...signed.headers
20588
22049
  },
20589
22050
  body
@@ -20592,14 +22053,14 @@ async function startConnectorRuntime(input) {
20592
22053
  if (response.status === 401) {
20593
22054
  throw new AppError({
20594
22055
  code: "OPENCLAW_RELAY_AGENT_AUTH_REJECTED",
20595
- message: "Peer relay rejected agent auth credentials",
22056
+ message: "Delivery receipt callback rejected agent auth credentials",
20596
22057
  status: 401,
20597
22058
  expose: true
20598
22059
  });
20599
22060
  }
20600
22061
  throw new AppError({
20601
- code: "CONNECTOR_OUTBOUND_DELIVERY_FAILED",
20602
- message: "Peer relay request failed",
22062
+ code: "CONNECTOR_DELIVERY_RECEIPT_FAILED",
22063
+ message: "Delivery receipt callback request failed",
20603
22064
  status: 502
20604
22065
  });
20605
22066
  }
@@ -20626,7 +22087,7 @@ async function startConnectorRuntime(input) {
20626
22087
  refreshToken: auth.refreshToken,
20627
22088
  fetchImpl
20628
22089
  }),
20629
- perform: performRelay
22090
+ perform: performReceipt
20630
22091
  });
20631
22092
  };
20632
22093
  const server = createServer(async (req, res) => {
@@ -20653,25 +22114,89 @@ async function startConnectorRuntime(input) {
20653
22114
  },
20654
22115
  outboundUrl,
20655
22116
  websocketUrl: wsUrl,
20656
- websocketConnected: connectorClient.isConnected()
22117
+ websocket: {
22118
+ connected: connectorClient.isConnected()
22119
+ }
20657
22120
  });
20658
22121
  return;
20659
22122
  }
22123
+ const clientMetrics = connectorClient.getMetricsSnapshot();
20660
22124
  writeJson(res, 200, {
20661
22125
  status: "ok",
20662
22126
  outboundUrl,
20663
22127
  websocketUrl: wsUrl,
20664
- websocketConnected: connectorClient.isConnected(),
20665
- inboundInbox: {
20666
- pendingCount: inboundReplayView.pending.pendingCount,
20667
- pendingBytes: inboundReplayView.pending.pendingBytes,
20668
- oldestPendingAt: inboundReplayView.pending.oldestPendingAt,
20669
- nextAttemptAt: inboundReplayView.pending.nextAttemptAt,
20670
- replayerActive: inboundReplayView.replayerActive,
20671
- lastReplayAt: inboundReplayView.lastReplayAt,
20672
- lastReplayError: inboundReplayView.lastReplayError
22128
+ websocket: {
22129
+ ...clientMetrics.connection
20673
22130
  },
20674
- openclawHook: inboundReplayView.openclawHook
22131
+ inbound: {
22132
+ pending: inboundReplayView.snapshot.pending,
22133
+ deadLetter: inboundReplayView.snapshot.deadLetter,
22134
+ replay: {
22135
+ replayerActive: inboundReplayView.replayerActive,
22136
+ lastReplayAt: inboundReplayView.lastReplayAt,
22137
+ lastReplayError: inboundReplayView.lastReplayError
22138
+ },
22139
+ openclawGateway: inboundReplayView.openclawGateway,
22140
+ openclawHook: inboundReplayView.openclawHook
22141
+ },
22142
+ outbound: {
22143
+ queue: {
22144
+ pendingCount: connectorClient.getQueuedOutboundCount()
22145
+ }
22146
+ },
22147
+ metrics: {
22148
+ heartbeat: clientMetrics.heartbeat,
22149
+ inboundDelivery: clientMetrics.inboundDelivery,
22150
+ outboundQueue: clientMetrics.outboundQueue
22151
+ }
22152
+ });
22153
+ return;
22154
+ }
22155
+ if (requestPath === deadLetterPath) {
22156
+ if (req.method !== "GET") {
22157
+ res.statusCode = 405;
22158
+ res.setHeader("allow", "GET");
22159
+ writeJson(res, 405, { error: "Method Not Allowed" });
22160
+ return;
22161
+ }
22162
+ const deadLetterItems = await inboundInbox.listDeadLetter();
22163
+ writeJson(res, 200, {
22164
+ status: "ok",
22165
+ count: deadLetterItems.length,
22166
+ items: deadLetterItems
22167
+ });
22168
+ return;
22169
+ }
22170
+ if (requestPath === deadLetterReplayPath) {
22171
+ if (req.method !== "POST") {
22172
+ res.statusCode = 405;
22173
+ res.setHeader("allow", "POST");
22174
+ writeJson(res, 405, { error: "Method Not Allowed" });
22175
+ return;
22176
+ }
22177
+ const body = await readRequestJson(req);
22178
+ const requestIds = isRecord6(body) ? parseRequestIds(body.requestIds) : void 0;
22179
+ const replayResult = await inboundInbox.replayDeadLetter({ requestIds });
22180
+ void replayPendingInboundMessages();
22181
+ writeJson(res, 200, {
22182
+ status: "ok",
22183
+ replayedCount: replayResult.replayedCount
22184
+ });
22185
+ return;
22186
+ }
22187
+ if (requestPath === deadLetterPurgePath) {
22188
+ if (req.method !== "POST") {
22189
+ res.statusCode = 405;
22190
+ res.setHeader("allow", "POST");
22191
+ writeJson(res, 405, { error: "Method Not Allowed" });
22192
+ return;
22193
+ }
22194
+ const body = await readRequestJson(req);
22195
+ const requestIds = isRecord6(body) ? parseRequestIds(body.requestIds) : void 0;
22196
+ const purgeResult = await inboundInbox.purgeDeadLetter({ requestIds });
22197
+ writeJson(res, 200, {
22198
+ status: "ok",
22199
+ purgedCount: purgeResult.purgedCount
20675
22200
  });
20676
22201
  return;
20677
22202
  }
@@ -20722,10 +22247,15 @@ async function startConnectorRuntime(input) {
20722
22247
  });
20723
22248
  const stop = async () => {
20724
22249
  runtimeStopping = true;
22250
+ runtimeShutdownController.abort();
20725
22251
  if (replayIntervalHandle !== void 0) {
20726
22252
  clearInterval(replayIntervalHandle);
20727
22253
  replayIntervalHandle = void 0;
20728
22254
  }
22255
+ if (openclawProbeIntervalHandle !== void 0) {
22256
+ clearInterval(openclawProbeIntervalHandle);
22257
+ openclawProbeIntervalHandle = void 0;
22258
+ }
20729
22259
  connectorClient.disconnect();
20730
22260
  await new Promise((resolve2, reject) => {
20731
22261
  server.close((error48) => {
@@ -20749,12 +22279,17 @@ async function startConnectorRuntime(input) {
20749
22279
  }
20750
22280
  );
20751
22281
  });
22282
+ await syncOpenclawHookToken("batch");
22283
+ await probeOpenclawGateway();
20752
22284
  connectorClient.connect();
20753
22285
  await inboundInbox.pruneDelivered();
20754
22286
  void replayPendingInboundMessages();
20755
22287
  replayIntervalHandle = setInterval(() => {
20756
22288
  void replayPendingInboundMessages();
20757
22289
  }, inboundReplayPolicy.replayIntervalMs);
22290
+ openclawProbeIntervalHandle = setInterval(() => {
22291
+ void probeOpenclawGateway();
22292
+ }, openclawProbePolicy.intervalMs);
20758
22293
  logger11.info("connector.runtime.started", {
20759
22294
  outboundUrl,
20760
22295
  websocketUrl: wsUrl,
@@ -20777,7 +22312,7 @@ var IDENTITY_FILE_NAME2 = "identity.json";
20777
22312
  var AIT_FILE_NAME2 = "ait.jwt";
20778
22313
  var SECRET_KEY_FILE_NAME = "secret.key";
20779
22314
  var REGISTRY_AUTH_FILE_NAME2 = "registry-auth.json";
20780
- var OPENCLAW_RELAY_RUNTIME_FILE_NAME = "openclaw-relay.json";
22315
+ var OPENCLAW_RELAY_RUNTIME_FILE_NAME2 = "openclaw-relay.json";
20781
22316
  var OPENCLAW_CONNECTORS_FILE_NAME = "openclaw-connectors.json";
20782
22317
  var SERVICE_LOG_DIR_NAME = "logs";
20783
22318
  var DEFAULT_CONNECTOR_BASE_URL2 = "http://127.0.0.1:19400";
@@ -20989,7 +22524,7 @@ async function readRequiredTrimmedFile(filePath, label, readFileImpl) {
20989
22524
  return trimmed;
20990
22525
  }
20991
22526
  async function readRelayRuntimeConfig(configDir, readFileImpl) {
20992
- const filePath = join6(configDir, OPENCLAW_RELAY_RUNTIME_FILE_NAME);
22527
+ const filePath = join6(configDir, OPENCLAW_RELAY_RUNTIME_FILE_NAME2);
20993
22528
  let raw;
20994
22529
  try {
20995
22530
  raw = await readFileImpl(filePath, "utf8");
@@ -21995,7 +23530,7 @@ var LEGACY_OPENCLAW_CONFIG_FILE_NAMES = [
21995
23530
  "moltbot.json"
21996
23531
  ];
21997
23532
  var OPENCLAW_AGENT_FILE_NAME = "openclaw-agent-name";
21998
- var OPENCLAW_RELAY_RUNTIME_FILE_NAME2 = "openclaw-relay.json";
23533
+ var OPENCLAW_RELAY_RUNTIME_FILE_NAME3 = "openclaw-relay.json";
21999
23534
  var OPENCLAW_CONNECTORS_FILE_NAME2 = "openclaw-connectors.json";
22000
23535
  var SKILL_DIR_NAME = "clawdentity-openclaw-relay";
22001
23536
  var RELAY_MODULE_FILE_NAME = "relay-to-peer.mjs";
@@ -22235,7 +23770,7 @@ function resolveOpenclawAgentNamePath(homeDir) {
22235
23770
  return join7(getConfigDir({ homeDir }), OPENCLAW_AGENT_FILE_NAME);
22236
23771
  }
22237
23772
  function resolveRelayRuntimeConfigPath(homeDir) {
22238
- return join7(getConfigDir({ homeDir }), OPENCLAW_RELAY_RUNTIME_FILE_NAME2);
23773
+ return join7(getConfigDir({ homeDir }), OPENCLAW_RELAY_RUNTIME_FILE_NAME3);
22239
23774
  }
22240
23775
  function resolveConnectorAssignmentsPath(homeDir) {
22241
23776
  return join7(getConfigDir({ homeDir }), OPENCLAW_CONNECTORS_FILE_NAME2);
@@ -22608,27 +24143,35 @@ function resolveConnectorStatusUrl(connectorBaseUrl) {
22608
24143
  ).toString();
22609
24144
  }
22610
24145
  function parseConnectorStatusPayload(payload) {
22611
- if (!isRecord9(payload) || typeof payload.websocketConnected !== "boolean") {
24146
+ if (!isRecord9(payload) || !isRecord9(payload.websocket) || typeof payload.websocket.connected !== "boolean") {
22612
24147
  throw createCliError6(
22613
24148
  "CLI_OPENCLAW_SETUP_CONNECTOR_STATUS_INVALID",
22614
24149
  "Connector status response is invalid"
22615
24150
  );
22616
24151
  }
24152
+ const inboundRoot = isRecord9(payload.inbound) ? payload.inbound : void 0;
24153
+ const pending = inboundRoot && isRecord9(inboundRoot.pending) ? inboundRoot.pending : void 0;
24154
+ const deadLetter = inboundRoot && isRecord9(inboundRoot.deadLetter) ? inboundRoot.deadLetter : void 0;
24155
+ const replay = inboundRoot && isRecord9(inboundRoot.replay) ? inboundRoot.replay : void 0;
24156
+ const hook = inboundRoot && isRecord9(inboundRoot.openclawHook) ? inboundRoot.openclawHook : void 0;
22617
24157
  return {
22618
- websocketConnected: payload.websocketConnected,
22619
- inboundInbox: isRecord9(payload.inboundInbox) ? {
22620
- pendingCount: typeof payload.inboundInbox.pendingCount === "number" ? payload.inboundInbox.pendingCount : void 0,
22621
- pendingBytes: typeof payload.inboundInbox.pendingBytes === "number" ? payload.inboundInbox.pendingBytes : void 0,
22622
- oldestPendingAt: typeof payload.inboundInbox.oldestPendingAt === "string" ? payload.inboundInbox.oldestPendingAt : void 0,
22623
- nextAttemptAt: typeof payload.inboundInbox.nextAttemptAt === "string" ? payload.inboundInbox.nextAttemptAt : void 0,
22624
- lastReplayAt: typeof payload.inboundInbox.lastReplayAt === "string" ? payload.inboundInbox.lastReplayAt : void 0,
22625
- lastReplayError: typeof payload.inboundInbox.lastReplayError === "string" ? payload.inboundInbox.lastReplayError : void 0,
22626
- replayerActive: typeof payload.inboundInbox.replayerActive === "boolean" ? payload.inboundInbox.replayerActive : void 0
24158
+ websocketConnected: payload.websocket.connected,
24159
+ inboundInbox: pending || deadLetter || replay ? {
24160
+ pendingCount: pending && typeof pending.pendingCount === "number" ? pending.pendingCount : void 0,
24161
+ pendingBytes: pending && typeof pending.pendingBytes === "number" ? pending.pendingBytes : void 0,
24162
+ oldestPendingAt: pending && typeof pending.oldestPendingAt === "string" ? pending.oldestPendingAt : void 0,
24163
+ nextAttemptAt: pending && typeof pending.nextAttemptAt === "string" ? pending.nextAttemptAt : void 0,
24164
+ lastReplayAt: replay && typeof replay.lastReplayAt === "string" ? replay.lastReplayAt : void 0,
24165
+ lastReplayError: replay && typeof replay.lastReplayError === "string" ? replay.lastReplayError : void 0,
24166
+ replayerActive: replay && typeof replay.replayerActive === "boolean" ? replay.replayerActive : void 0,
24167
+ deadLetterCount: deadLetter && typeof deadLetter.deadLetterCount === "number" ? deadLetter.deadLetterCount : void 0,
24168
+ deadLetterBytes: deadLetter && typeof deadLetter.deadLetterBytes === "number" ? deadLetter.deadLetterBytes : void 0,
24169
+ oldestDeadLetterAt: deadLetter && typeof deadLetter.oldestDeadLetterAt === "string" ? deadLetter.oldestDeadLetterAt : void 0
22627
24170
  } : void 0,
22628
- openclawHook: isRecord9(payload.openclawHook) ? {
22629
- url: typeof payload.openclawHook.url === "string" ? payload.openclawHook.url : void 0,
22630
- lastAttemptAt: typeof payload.openclawHook.lastAttemptAt === "string" ? payload.openclawHook.lastAttemptAt : void 0,
22631
- lastAttemptStatus: payload.openclawHook.lastAttemptStatus === "ok" || payload.openclawHook.lastAttemptStatus === "failed" ? payload.openclawHook.lastAttemptStatus : void 0
24171
+ openclawHook: hook ? {
24172
+ url: typeof hook.url === "string" ? hook.url : void 0,
24173
+ lastAttemptAt: typeof hook.lastAttemptAt === "string" ? hook.lastAttemptAt : void 0,
24174
+ lastAttemptStatus: hook.lastAttemptStatus === "ok" || hook.lastAttemptStatus === "failed" ? hook.lastAttemptStatus : void 0
22632
24175
  } : void 0
22633
24176
  };
22634
24177
  }
@@ -22679,12 +24222,12 @@ async function fetchConnectorHealthStatus(input) {
22679
24222
  }
22680
24223
  }
22681
24224
  async function waitForConnectorConnected(input) {
22682
- const deadline = Date.now() + input.waitTimeoutSeconds * 1e3;
24225
+ const deadline = nowUtcMs() + input.waitTimeoutSeconds * 1e3;
22683
24226
  let latest = await fetchConnectorHealthStatus({
22684
24227
  connectorBaseUrl: input.connectorBaseUrl,
22685
24228
  fetchImpl: input.fetchImpl
22686
24229
  });
22687
- while (!latest.connected && Date.now() < deadline) {
24230
+ while (!latest.connected && nowUtcMs() < deadline) {
22688
24231
  await new Promise((resolve2) => {
22689
24232
  setTimeout(resolve2, 1e3);
22690
24233
  });
@@ -22718,7 +24261,7 @@ async function monitorConnectorStabilityWindow(input) {
22718
24261
  fetchImpl: input.fetchImpl
22719
24262
  });
22720
24263
  }
22721
- const deadline = Date.now() + input.durationSeconds * 1e3;
24264
+ const deadline = nowUtcMs() + input.durationSeconds * 1e3;
22722
24265
  let latest = await fetchConnectorHealthStatus({
22723
24266
  connectorBaseUrl: input.connectorBaseUrl,
22724
24267
  fetchImpl: input.fetchImpl
@@ -22726,7 +24269,7 @@ async function monitorConnectorStabilityWindow(input) {
22726
24269
  if (!latest.connected) {
22727
24270
  return latest;
22728
24271
  }
22729
- while (Date.now() < deadline) {
24272
+ while (nowUtcMs() < deadline) {
22730
24273
  await sleepMilliseconds(input.pollIntervalMs);
22731
24274
  latest = await fetchConnectorHealthStatus({
22732
24275
  connectorBaseUrl: input.connectorBaseUrl,
@@ -23230,10 +24773,80 @@ function printRelayTestResult(result) {
23230
24773
  writeStdoutLine(`Fix: ${result.remediationHint}`);
23231
24774
  }
23232
24775
  }
24776
+ function printRelayWebsocketTestResult(result) {
24777
+ writeStdoutLine(`Relay websocket test status: ${result.status}`);
24778
+ writeStdoutLine(`Peer alias: ${result.peerAlias}`);
24779
+ if (typeof result.connectorBaseUrl === "string") {
24780
+ writeStdoutLine(`Connector base URL: ${result.connectorBaseUrl}`);
24781
+ }
24782
+ if (typeof result.connectorStatusUrl === "string") {
24783
+ writeStdoutLine(`Connector status URL: ${result.connectorStatusUrl}`);
24784
+ }
24785
+ writeStdoutLine(`Message: ${result.message}`);
24786
+ if (result.remediationHint) {
24787
+ writeStdoutLine(`Fix: ${result.remediationHint}`);
24788
+ }
24789
+ }
23233
24790
  function toSendToPeerEndpoint(openclawBaseUrl) {
23234
24791
  const normalizedBase = openclawBaseUrl.endsWith("/") ? openclawBaseUrl : `${openclawBaseUrl}/`;
23235
24792
  return new URL(OPENCLAW_SEND_TO_PEER_HOOK_PATH, normalizedBase).toString();
23236
24793
  }
24794
+ async function resolveSelectedAgentName(input) {
24795
+ const selectedAgentPath = resolveOpenclawAgentNamePath(input.homeDir);
24796
+ let selectedAgentRaw;
24797
+ try {
24798
+ selectedAgentRaw = await readFile6(selectedAgentPath, "utf8");
24799
+ } catch (error48) {
24800
+ if (getErrorCode2(error48) === "ENOENT") {
24801
+ throw createCliError6(
24802
+ "CLI_OPENCLAW_SELECTED_AGENT_MISSING",
24803
+ "Selected agent marker is missing",
24804
+ { selectedAgentPath }
24805
+ );
24806
+ }
24807
+ throw createCliError6(
24808
+ "CLI_OPENCLAW_SELECTED_AGENT_INVALID",
24809
+ "Selected agent marker is invalid",
24810
+ { selectedAgentPath }
24811
+ );
24812
+ }
24813
+ try {
24814
+ return {
24815
+ agentName: assertValidAgentName(selectedAgentRaw.trim()),
24816
+ selectedAgentPath
24817
+ };
24818
+ } catch {
24819
+ throw createCliError6(
24820
+ "CLI_OPENCLAW_SELECTED_AGENT_INVALID",
24821
+ "Selected agent marker is invalid",
24822
+ { selectedAgentPath }
24823
+ );
24824
+ }
24825
+ }
24826
+ async function resolveConnectorAssignment(input) {
24827
+ const connectorAssignmentsPath = resolveConnectorAssignmentsPath(
24828
+ input.homeDir
24829
+ );
24830
+ const connectorAssignments = await loadConnectorAssignments(
24831
+ connectorAssignmentsPath
24832
+ );
24833
+ const assignment = connectorAssignments.agents[input.agentName];
24834
+ if (assignment === void 0) {
24835
+ throw createCliError6(
24836
+ "CLI_OPENCLAW_CONNECTOR_ASSIGNMENT_MISSING",
24837
+ "Connector assignment is missing for selected agent",
24838
+ {
24839
+ connectorAssignmentsPath,
24840
+ agentName: input.agentName
24841
+ }
24842
+ );
24843
+ }
24844
+ return {
24845
+ connectorAssignmentsPath,
24846
+ connectorBaseUrl: assignment.connectorBaseUrl,
24847
+ connectorStatusUrl: resolveConnectorStatusUrl(assignment.connectorBaseUrl)
24848
+ };
24849
+ }
23237
24850
  async function runOpenclawDoctor(options = {}) {
23238
24851
  const homeDir = resolveHomeDir2(options.homeDir);
23239
24852
  const openclawDir = resolveOpenclawDir(options.openclawDir, homeDir);
@@ -24245,6 +25858,111 @@ async function runOpenclawRelayTest(options) {
24245
25858
  preflight
24246
25859
  };
24247
25860
  }
25861
+ async function runOpenclawRelayWebsocketTest(options) {
25862
+ const homeDir = resolveHomeDir2(options.homeDir);
25863
+ const openclawDir = resolveOpenclawDir(options.openclawDir, homeDir);
25864
+ const checkedAt = nowIso();
25865
+ let peerAlias;
25866
+ try {
25867
+ peerAlias = await resolveRelayProbePeerAlias({
25868
+ homeDir,
25869
+ peerAliasOption: options.peer
25870
+ });
25871
+ } catch (error48) {
25872
+ const appError = error48 instanceof AppError ? error48 : void 0;
25873
+ return {
25874
+ status: "failure",
25875
+ checkedAt,
25876
+ peerAlias: "unresolved",
25877
+ message: appError?.message ?? "Unable to resolve relay peer alias",
25878
+ remediationHint: OPENCLAW_PAIRING_COMMAND_HINT,
25879
+ details: appError?.details
25880
+ };
25881
+ }
25882
+ const preflight = await runOpenclawDoctor({
25883
+ homeDir,
25884
+ openclawDir,
25885
+ peerAlias,
25886
+ resolveConfigImpl: options.resolveConfigImpl,
25887
+ includeConnectorRuntimeCheck: false
25888
+ });
25889
+ if (preflight.status === "unhealthy") {
25890
+ const firstFailure = preflight.checks.find(
25891
+ (check2) => check2.status === "fail"
25892
+ );
25893
+ return {
25894
+ status: "failure",
25895
+ checkedAt,
25896
+ peerAlias,
25897
+ message: firstFailure === void 0 ? "Preflight checks failed" : `Preflight failed: ${firstFailure.label}`,
25898
+ remediationHint: firstFailure?.remediationHint,
25899
+ preflight
25900
+ };
25901
+ }
25902
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
25903
+ if (typeof fetchImpl !== "function") {
25904
+ return {
25905
+ status: "failure",
25906
+ checkedAt,
25907
+ peerAlias,
25908
+ message: "fetch implementation is unavailable",
25909
+ remediationHint: "Run relay websocket test in a Node runtime with fetch support",
25910
+ preflight
25911
+ };
25912
+ }
25913
+ let connectorBaseUrl;
25914
+ let connectorStatusUrl;
25915
+ try {
25916
+ const selectedAgent = await resolveSelectedAgentName({ homeDir });
25917
+ const connectorAssignment = await resolveConnectorAssignment({
25918
+ homeDir,
25919
+ agentName: selectedAgent.agentName
25920
+ });
25921
+ connectorBaseUrl = connectorAssignment.connectorBaseUrl;
25922
+ connectorStatusUrl = connectorAssignment.connectorStatusUrl;
25923
+ } catch (error48) {
25924
+ const appError = error48 instanceof AppError ? error48 : void 0;
25925
+ return {
25926
+ status: "failure",
25927
+ checkedAt,
25928
+ peerAlias,
25929
+ connectorBaseUrl,
25930
+ connectorStatusUrl,
25931
+ message: appError?.message ?? "Unable to resolve connector assignment for websocket test",
25932
+ remediationHint: OPENCLAW_SETUP_COMMAND_HINT,
25933
+ details: appError?.details,
25934
+ preflight
25935
+ };
25936
+ }
25937
+ const connectorStatus = await fetchConnectorHealthStatus({
25938
+ connectorBaseUrl,
25939
+ fetchImpl
25940
+ });
25941
+ if (!connectorStatus.connected) {
25942
+ return {
25943
+ status: "failure",
25944
+ checkedAt,
25945
+ peerAlias,
25946
+ connectorBaseUrl,
25947
+ connectorStatusUrl: connectorStatus.statusUrl,
25948
+ message: "Connector websocket is not connected",
25949
+ remediationHint: OPENCLAW_SETUP_COMMAND_HINT,
25950
+ details: connectorStatus.reason === void 0 ? void 0 : {
25951
+ reason: connectorStatus.reason
25952
+ },
25953
+ preflight
25954
+ };
25955
+ }
25956
+ return {
25957
+ status: "success",
25958
+ checkedAt,
25959
+ peerAlias,
25960
+ connectorBaseUrl,
25961
+ connectorStatusUrl: connectorStatus.statusUrl,
25962
+ message: "Connector websocket is connected for paired relay",
25963
+ preflight
25964
+ };
25965
+ }
24248
25966
  async function setupOpenclawRelay(agentName, options) {
24249
25967
  const normalizedAgentName = assertValidAgentName(agentName);
24250
25968
  const homeDir = resolveHomeDir2(options.homeDir);
@@ -24629,6 +26347,38 @@ var createOpenclawCommand = () => {
24629
26347
  }
24630
26348
  )
24631
26349
  );
26350
+ relayCommand.command("ws-test").description(
26351
+ "Validate connector websocket connectivity for a paired relay peer"
26352
+ ).option("--peer <alias>", "Peer alias in local peers map").option(
26353
+ "--openclaw-dir <path>",
26354
+ "OpenClaw state directory (default ~/.openclaw)"
26355
+ ).option("--json", "Print machine-readable JSON output").action(
26356
+ withErrorHandling(
26357
+ "openclaw relay ws-test",
26358
+ async (options) => {
26359
+ const result = await runOpenclawRelayWebsocketTest(options);
26360
+ if (options.json) {
26361
+ writeStdoutLine(JSON.stringify(result, null, 2));
26362
+ } else {
26363
+ printRelayWebsocketTestResult(result);
26364
+ if (result.preflight !== void 0 && result.preflight.status === "unhealthy") {
26365
+ writeStdoutLine("Preflight details:");
26366
+ for (const check2 of result.preflight.checks) {
26367
+ if (check2.status === "fail") {
26368
+ writeStdoutLine(formatDoctorCheckLine(check2));
26369
+ if (check2.remediationHint) {
26370
+ writeStdoutLine(`Fix: ${check2.remediationHint}`);
26371
+ }
26372
+ }
26373
+ }
26374
+ }
26375
+ }
26376
+ if (result.status === "failure") {
26377
+ process.exitCode = 1;
26378
+ }
26379
+ }
26380
+ )
26381
+ );
24632
26382
  return openclawCommand;
24633
26383
  };
24634
26384
 
@@ -24639,7 +26389,7 @@ import {
24639
26389
  mkdir as mkdir7,
24640
26390
  readdir as readdir2,
24641
26391
  readFile as readFile7,
24642
- unlink as unlink2,
26392
+ unlink as unlink3,
24643
26393
  writeFile as writeFile7
24644
26394
  } from "fs/promises";
24645
26395
  import { dirname as dirname6, join as join8, resolve } from "path";
@@ -24653,7 +26403,7 @@ var AIT_FILE_NAME4 = "ait.jwt";
24653
26403
  var SECRET_KEY_FILE_NAME3 = "secret.key";
24654
26404
  var PAIRING_QR_DIR_NAME = "pairing";
24655
26405
  var PEERS_FILE_NAME2 = "peers.json";
24656
- var OPENCLAW_RELAY_RUNTIME_FILE_NAME3 = "openclaw-relay.json";
26406
+ var OPENCLAW_RELAY_RUNTIME_FILE_NAME4 = "openclaw-relay.json";
24657
26407
  var PAIR_START_PATH = "/pair/start";
24658
26408
  var PAIR_CONFIRM_PATH = "/pair/confirm";
24659
26409
  var PAIR_STATUS_PATH = "/pair/status";
@@ -24669,6 +26419,7 @@ var MAX_PROFILE_NAME_LENGTH = 64;
24669
26419
  var isRecord10 = (value) => {
24670
26420
  return typeof value === "object" && value !== null;
24671
26421
  };
26422
+ var nowUnixSeconds = () => Math.floor(nowUtcMs() / 1e3);
24672
26423
  function createCliError7(code, message2) {
24673
26424
  return new AppError({
24674
26425
  code,
@@ -24720,10 +26471,24 @@ function parsePeerProfile(payload) {
24720
26471
  "Pair profile must be an object"
24721
26472
  );
24722
26473
  }
24723
- return {
26474
+ const profile = {
24724
26475
  agentName: parseProfileName(payload.agentName, "agentName"),
24725
26476
  humanName: parseProfileName(payload.humanName, "humanName")
24726
26477
  };
26478
+ const proxyOrigin = parseNonEmptyString10(payload.proxyOrigin);
26479
+ if (proxyOrigin.length > 0) {
26480
+ let parsedProxyOrigin;
26481
+ try {
26482
+ parsedProxyOrigin = new URL(parseProxyUrl2(proxyOrigin)).origin;
26483
+ } catch {
26484
+ throw createCliError7(
26485
+ "CLI_PAIR_PROFILE_INVALID",
26486
+ "proxyOrigin is invalid for pairing"
26487
+ );
26488
+ }
26489
+ profile.proxyOrigin = parsedProxyOrigin;
26490
+ }
26491
+ return profile;
24727
26492
  }
24728
26493
  function parsePairingTicket(value) {
24729
26494
  let ticket = parseNonEmptyString10(value);
@@ -24992,7 +26757,7 @@ async function savePeersConfig2(input) {
24992
26757
  await input.chmodImpl(peersPath, FILE_MODE4);
24993
26758
  }
24994
26759
  function resolveRelayRuntimeConfigPath2(getConfigDirImpl) {
24995
- return join8(getConfigDirImpl(), OPENCLAW_RELAY_RUNTIME_FILE_NAME3);
26760
+ return join8(getConfigDirImpl(), OPENCLAW_RELAY_RUNTIME_FILE_NAME4);
24996
26761
  }
24997
26762
  async function loadRelayTransformPeersPath(input) {
24998
26763
  const relayRuntimeConfigPath = resolveRelayRuntimeConfigPath2(
@@ -25107,10 +26872,59 @@ function resolveLocalPairProfile(input) {
25107
26872
  "Human name is missing. Run `clawdentity invite redeem <clw_inv_...> --display-name <name>` or `clawdentity config set humanName <name>`."
25108
26873
  );
25109
26874
  }
25110
- return {
26875
+ const profile = {
25111
26876
  agentName: parseProfileName(input.agentName, "agentName"),
25112
26877
  humanName: parseProfileName(humanName, "humanName")
25113
26878
  };
26879
+ const proxyUrl = parseNonEmptyString10(input.proxyUrl);
26880
+ if (proxyUrl.length > 0) {
26881
+ profile.proxyOrigin = new URL(parseProxyUrl2(proxyUrl)).origin;
26882
+ }
26883
+ return profile;
26884
+ }
26885
+ function normalizeProxyOrigin(candidate) {
26886
+ return new URL(parseProxyUrl2(candidate)).origin;
26887
+ }
26888
+ function resolvePeerProxyUrl(input) {
26889
+ const configuredPeerOrigin = parseNonEmptyString10(input.peerProxyOrigin);
26890
+ const profilePeerOrigin = parseNonEmptyString10(input.peerProfile.proxyOrigin);
26891
+ const fallbackPeerOrigin = parsePairingTicketIssuerOrigin(input.ticket);
26892
+ const peerOrigin = configuredPeerOrigin.length > 0 ? configuredPeerOrigin : profilePeerOrigin.length > 0 ? profilePeerOrigin : fallbackPeerOrigin;
26893
+ return new URL(
26894
+ "/hooks/agent",
26895
+ `${normalizeProxyOrigin(peerOrigin)}/`
26896
+ ).toString();
26897
+ }
26898
+ function toIssuerProxyUrl(ticket) {
26899
+ return parseProxyUrl2(parsePairingTicketIssuerOrigin(ticket));
26900
+ }
26901
+ function toIssuerProxyRequestUrl(ticket, path) {
26902
+ return toProxyRequestUrl(toIssuerProxyUrl(ticket), path);
26903
+ }
26904
+ function toPeerProxyOriginFromStatus(input) {
26905
+ if (input.callerAgentDid === input.initiatorAgentDid) {
26906
+ return input.responderProfile?.proxyOrigin;
26907
+ }
26908
+ if (input.callerAgentDid === input.responderAgentDid) {
26909
+ return input.initiatorProfile.proxyOrigin;
26910
+ }
26911
+ return void 0;
26912
+ }
26913
+ function toPeerProxyOriginFromConfirm(input) {
26914
+ const initiatorOrigin = parseNonEmptyString10(
26915
+ input.initiatorProfile.proxyOrigin
26916
+ );
26917
+ if (initiatorOrigin.length > 0) {
26918
+ return initiatorOrigin;
26919
+ }
26920
+ return parsePairingTicketIssuerOrigin(input.ticket);
26921
+ }
26922
+ function toResponderProfile(input) {
26923
+ return resolveLocalPairProfile({
26924
+ config: input.config,
26925
+ agentName: input.agentName,
26926
+ proxyUrl: input.localProxyUrl
26927
+ });
25114
26928
  }
25115
26929
  function parseProxyUrl2(candidate) {
25116
26930
  try {
@@ -25507,7 +27321,7 @@ function decodeTicketFromPng(imageBytes) {
25507
27321
  async function persistPairingQr(input) {
25508
27322
  const mkdirImpl = input.dependencies.mkdirImpl ?? mkdir7;
25509
27323
  const readdirImpl = input.dependencies.readdirImpl ?? readdir2;
25510
- const unlinkImpl = input.dependencies.unlinkImpl ?? unlink2;
27324
+ const unlinkImpl = input.dependencies.unlinkImpl ?? unlink3;
25511
27325
  const writeFileImpl = input.dependencies.writeFileImpl ?? writeFile7;
25512
27326
  const getConfigDirImpl = input.dependencies.getConfigDirImpl ?? getConfigDir;
25513
27327
  const qrEncodeImpl = input.dependencies.qrEncodeImpl ?? encodeTicketQrPng;
@@ -25585,8 +27399,11 @@ async function persistPairedPeer(input) {
25585
27399
  const mkdirImpl = input.dependencies.mkdirImpl ?? mkdir7;
25586
27400
  const writeFileImpl = input.dependencies.writeFileImpl ?? writeFile7;
25587
27401
  const chmodImpl = input.dependencies.chmodImpl ?? chmod4;
25588
- const issuerOrigin = parsePairingTicketIssuerOrigin(input.ticket);
25589
- const peerProxyUrl = new URL("/hooks/agent", `${issuerOrigin}/`).toString();
27402
+ const peerProxyUrl = resolvePeerProxyUrl({
27403
+ ticket: input.ticket,
27404
+ peerProfile: input.peerProfile,
27405
+ peerProxyOrigin: input.peerProxyOrigin
27406
+ });
25590
27407
  const peersConfig = await loadPeersConfig2({
25591
27408
  getConfigDirImpl,
25592
27409
  readFileImpl
@@ -25621,7 +27438,7 @@ async function persistPairedPeer(input) {
25621
27438
  async function startPairing(agentName, options, dependencies = {}) {
25622
27439
  const fetchImpl = dependencies.fetchImpl ?? fetch;
25623
27440
  const resolveConfigImpl = dependencies.resolveConfigImpl ?? resolveConfig;
25624
- const nowSecondsImpl = dependencies.nowSecondsImpl ?? (() => Math.floor(Date.now() / 1e3));
27441
+ const nowSecondsImpl = dependencies.nowSecondsImpl ?? nowUnixSeconds;
25625
27442
  const nonceFactoryImpl = dependencies.nonceFactoryImpl ?? (() => randomBytes4(NONCE_SIZE2).toString("base64url"));
25626
27443
  const ttlSeconds = parseTtlSeconds(options.ttlSeconds);
25627
27444
  const config2 = await resolveConfigImpl();
@@ -25632,7 +27449,8 @@ async function startPairing(agentName, options, dependencies = {}) {
25632
27449
  const normalizedAgentName = assertValidAgentName(agentName);
25633
27450
  const initiatorProfile = resolveLocalPairProfile({
25634
27451
  config: config2,
25635
- agentName: normalizedAgentName
27452
+ agentName: normalizedAgentName,
27453
+ proxyUrl
25636
27454
  });
25637
27455
  const { ait, secretKey } = await readAgentProofMaterial(
25638
27456
  normalizedAgentName,
@@ -25693,21 +27511,22 @@ async function startPairing(agentName, options, dependencies = {}) {
25693
27511
  async function confirmPairing(agentName, options, dependencies = {}) {
25694
27512
  const fetchImpl = dependencies.fetchImpl ?? fetch;
25695
27513
  const resolveConfigImpl = dependencies.resolveConfigImpl ?? resolveConfig;
25696
- const nowSecondsImpl = dependencies.nowSecondsImpl ?? (() => Math.floor(Date.now() / 1e3));
27514
+ const nowSecondsImpl = dependencies.nowSecondsImpl ?? nowUnixSeconds;
25697
27515
  const nonceFactoryImpl = dependencies.nonceFactoryImpl ?? (() => randomBytes4(NONCE_SIZE2).toString("base64url"));
25698
27516
  const readFileImpl = dependencies.readFileImpl ?? readFile7;
25699
27517
  const qrDecodeImpl = dependencies.qrDecodeImpl ?? decodeTicketFromPng;
25700
27518
  const config2 = await resolveConfigImpl();
25701
27519
  const normalizedAgentName = assertValidAgentName(agentName);
25702
- const responderProfile = resolveLocalPairProfile({
27520
+ const localProxyUrl = await resolveProxyUrl({
25703
27521
  config: config2,
25704
- agentName: normalizedAgentName
27522
+ fetchImpl
25705
27523
  });
25706
- const ticketSource = resolveConfirmTicketSource(options);
25707
- const proxyUrl = await resolveProxyUrl({
27524
+ const responderProfile = toResponderProfile({
25708
27525
  config: config2,
25709
- fetchImpl
27526
+ agentName: normalizedAgentName,
27527
+ localProxyUrl
25710
27528
  });
27529
+ const ticketSource = resolveConfirmTicketSource(options);
25711
27530
  let ticket = ticketSource.ticket;
25712
27531
  if (ticketSource.source === "qr-file") {
25713
27532
  if (!ticketSource.qrFilePath) {
@@ -25732,16 +27551,12 @@ async function confirmPairing(agentName, options, dependencies = {}) {
25732
27551
  ticket = parsePairingTicket(qrDecodeImpl(new Uint8Array(imageBytes)));
25733
27552
  }
25734
27553
  ticket = parsePairingTicket(ticket);
25735
- assertTicketIssuerMatchesProxy({
25736
- ticket,
25737
- proxyUrl,
25738
- context: "confirm"
25739
- });
27554
+ const proxyUrl = toIssuerProxyUrl(ticket);
25740
27555
  const { ait, secretKey } = await readAgentProofMaterial(
25741
27556
  normalizedAgentName,
25742
27557
  dependencies
25743
27558
  );
25744
- const requestUrl = toProxyRequestUrl(proxyUrl, PAIR_CONFIRM_PATH);
27559
+ const requestUrl = toIssuerProxyRequestUrl(ticket, PAIR_CONFIRM_PATH);
25745
27560
  const requestBody = JSON.stringify({
25746
27561
  ticket,
25747
27562
  responderProfile
@@ -25778,14 +27593,19 @@ async function confirmPairing(agentName, options, dependencies = {}) {
25778
27593
  );
25779
27594
  }
25780
27595
  const parsed = parsePairConfirmResponse(responseBody);
27596
+ const peerProxyOrigin = toPeerProxyOriginFromConfirm({
27597
+ ticket,
27598
+ initiatorProfile: parsed.initiatorProfile
27599
+ });
25781
27600
  const peerAlias = await persistPairedPeer({
25782
27601
  ticket,
25783
27602
  peerDid: parsed.initiatorAgentDid,
25784
27603
  peerProfile: parsed.initiatorProfile,
27604
+ peerProxyOrigin,
25785
27605
  dependencies
25786
27606
  });
25787
27607
  if (ticketSource.source === "qr-file" && ticketSource.qrFilePath) {
25788
- const unlinkImpl = dependencies.unlinkImpl ?? unlink2;
27608
+ const unlinkImpl = dependencies.unlinkImpl ?? unlink3;
25789
27609
  await unlinkImpl(ticketSource.qrFilePath).catch((error48) => {
25790
27610
  const nodeError = error48;
25791
27611
  if (nodeError.code === "ENOENT") {
@@ -25806,7 +27626,7 @@ async function confirmPairing(agentName, options, dependencies = {}) {
25806
27626
  async function getPairingStatusOnce(agentName, options, dependencies = {}) {
25807
27627
  const fetchImpl = dependencies.fetchImpl ?? fetch;
25808
27628
  const resolveConfigImpl = dependencies.resolveConfigImpl ?? resolveConfig;
25809
- const nowSecondsImpl = dependencies.nowSecondsImpl ?? (() => Math.floor(Date.now() / 1e3));
27629
+ const nowSecondsImpl = dependencies.nowSecondsImpl ?? nowUnixSeconds;
25810
27630
  const nonceFactoryImpl = dependencies.nonceFactoryImpl ?? (() => randomBytes4(NONCE_SIZE2).toString("base64url"));
25811
27631
  const config2 = await resolveConfigImpl();
25812
27632
  const proxyUrl = await resolveProxyUrl({
@@ -25885,6 +27705,13 @@ async function getPairingStatusOnce(agentName, options, dependencies = {}) {
25885
27705
  ticket,
25886
27706
  peerDid,
25887
27707
  peerProfile,
27708
+ peerProxyOrigin: toPeerProxyOriginFromStatus({
27709
+ callerAgentDid,
27710
+ initiatorAgentDid: parsed.initiatorAgentDid,
27711
+ responderAgentDid,
27712
+ initiatorProfile: parsed.initiatorProfile,
27713
+ responderProfile: parsed.responderProfile
27714
+ }),
25888
27715
  dependencies
25889
27716
  });
25890
27717
  }
@@ -25895,7 +27722,7 @@ async function getPairingStatusOnce(agentName, options, dependencies = {}) {
25895
27722
  };
25896
27723
  }
25897
27724
  async function waitForPairingStatus(input) {
25898
- const nowSecondsImpl = input.dependencies.nowSecondsImpl ?? (() => Math.floor(Date.now() / 1e3));
27725
+ const nowSecondsImpl = input.dependencies.nowSecondsImpl ?? nowUnixSeconds;
25899
27726
  const sleepImpl = input.dependencies.sleepImpl ?? (async (ms) => {
25900
27727
  await new Promise((resolve2) => {
25901
27728
  setTimeout(resolve2, ms);
@@ -26640,7 +28467,7 @@ var fetchRegistryKeys = async (registryUrl) => {
26640
28467
  return parseSigningKeys(await parseResponseJson(response));
26641
28468
  };
26642
28469
  var loadRegistryKeys = async (registryUrl) => {
26643
- const now = Date.now();
28470
+ const now = nowUtcMs();
26644
28471
  const rawCache = await readCacheFile(REGISTRY_KEYS_CACHE_FILE);
26645
28472
  const cache2 = typeof rawCache === "string" ? parseRegistryKeysCache(rawCache) : void 0;
26646
28473
  const isFresh = isFreshCache({
@@ -26699,7 +28526,7 @@ var fetchCrlClaims = async (input) => {
26699
28526
  }
26700
28527
  };
26701
28528
  var loadCrlClaims = async (input) => {
26702
- const now = Date.now();
28529
+ const now = nowUtcMs();
26703
28530
  const rawCache = await readCacheFile(CRL_CLAIMS_CACHE_FILE);
26704
28531
  const cache2 = typeof rawCache === "string" ? parseCrlCache(rawCache) : void 0;
26705
28532
  const isFresh = isFreshCache({