@openclaw/gateway-client 2026.7.2-beta.7 → 2026.8.1-beta.2

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.
@@ -1,4 +1,4 @@
1
- import { clearGatewayConnectTimeout, startGatewayConnectTimeout } from "./timeouts.mjs";
1
+ import { clearGatewayConnectTimeout, resolveSafeTimeoutDelayMs, startGatewayConnectTimeout } from "./timeouts.mjs";
2
2
  import { ConnectErrorDetailCodes, readConnectErrorDetailCode, readConnectErrorRecoveryAdvice, readPairingConnectErrorDetails } from "@openclaw/gateway-protocol/connect-error-details";
3
3
  import { isGatewayEventFrame, isGatewayResponseFrame } from "@openclaw/gateway-protocol/frame-guards";
4
4
  //#region packages/gateway-client/src/device-auth.ts
@@ -59,7 +59,7 @@ function selectGatewayConnectAuth(params) {
59
59
  };
60
60
  if (params.preferBootstrapToken && bootstrapToken) return {
61
61
  authBootstrapToken: bootstrapToken,
62
- authPassword,
62
+ signatureToken: bootstrapToken,
63
63
  ...stored
64
64
  };
65
65
  const useRetryToken = params.pendingDeviceTokenRetry === true && !explicitDeviceToken && Boolean(authToken && storedToken && params.trustedDeviceTokenRetry);
@@ -139,7 +139,8 @@ var GatewayBrowserDeviceAuthLifecycle = class {
139
139
  scopes,
140
140
  auth: buildGatewayConnectAuth(selectedAuth)
141
141
  };
142
- const signedAtMs = this.deps.nowMs?.() ?? Date.now();
142
+ const signedAtMs = params.challengeTs === void 0 ? this.deps.nowMs?.() ?? Date.now() : params.challengeTs;
143
+ if (typeof signedAtMs !== "number" || !Number.isSafeInteger(signedAtMs) || signedAtMs < 0) throw new Error("gateway connect challenge timestamp invalid");
143
144
  const nonce = params.nonce ?? "";
144
145
  const { authBootstrapToken: primary, signatureToken: signed } = selectedAuth;
145
146
  let token = null;
@@ -176,12 +177,18 @@ var GatewayBrowserDeviceAuthLifecycle = class {
176
177
  async acceptHello(hello, plan) {
177
178
  const token = hello.auth?.deviceToken?.trim();
178
179
  if (!token || !plan.identity) return;
180
+ const role = hello.auth?.role ?? plan.role;
181
+ const stored = await this.deps.tokenStore.load({
182
+ clientId: plan.clientId,
183
+ deviceId: plan.identity.deviceId,
184
+ role
185
+ });
179
186
  await this.deps.tokenStore.store({
180
187
  clientId: plan.clientId,
181
188
  deviceId: plan.identity.deviceId,
182
- role: hello.auth?.role ?? plan.role,
189
+ role,
183
190
  token,
184
- scopes: hello.auth?.scopes ?? []
191
+ scopes: stored?.token === token ? stored.scopes : hello.auth?.scopes ?? []
185
192
  });
186
193
  }
187
194
  async clearStoredToken(plan) {
@@ -194,6 +201,29 @@ var GatewayBrowserDeviceAuthLifecycle = class {
194
201
  }
195
202
  };
196
203
  //#endregion
204
+ //#region packages/gateway-client/src/gateway-origin-scope.ts
205
+ function normalizeGatewayScope(gatewayUrl, includeSearch) {
206
+ const trimmed = gatewayUrl.trim();
207
+ if (!trimmed) return "default";
208
+ try {
209
+ const browserLocation = globalThis.location;
210
+ const base = browserLocation ? `${browserLocation.protocol}//${browserLocation.host}${browserLocation.pathname || "/"}` : void 0;
211
+ const parsed = base ? new URL(trimmed, base) : new URL(trimmed);
212
+ const pathname = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/+$/, "") || parsed.pathname;
213
+ return `${parsed.protocol}//${parsed.host}${pathname}${includeSearch ? parsed.search : ""}`;
214
+ } catch {
215
+ return trimmed;
216
+ }
217
+ }
218
+ /** Normalizes the gateway URL scope used for origin-bound device tokens. */
219
+ function gatewayOriginScope(gatewayUrl) {
220
+ return normalizeGatewayScope(gatewayUrl, false);
221
+ }
222
+ /** Normalizes the gateway URL scope used for browser credential records. */
223
+ function gatewayCredentialScope(gatewayUrl) {
224
+ return normalizeGatewayScope(gatewayUrl, true);
225
+ }
226
+ //#endregion
197
227
  //#region packages/retry/src/index.ts
198
228
  const MAX_TIMER_TIMEOUT_MS = 2147e6;
199
229
  function computeBackoff(policy, attempt) {
@@ -214,7 +244,9 @@ async function sleepWithAbort(ms, abortSignal, options = {}) {
214
244
  if (timer) clearTimeout(timer);
215
245
  timer = null;
216
246
  cleanup();
217
- reject(new Error("aborted", { cause: abortSignal?.reason ?? /* @__PURE__ */ new Error("aborted") }));
247
+ const error = new Error("aborted", { cause: abortSignal?.reason ?? /* @__PURE__ */ new Error("aborted") });
248
+ error.name = "AbortError";
249
+ reject(error);
218
250
  };
219
251
  abortSignal?.addEventListener("abort", onAbort, { once: true });
220
252
  if (abortSignal?.aborted) {
@@ -276,24 +308,21 @@ const DEFAULT_RETRY_CONFIG = {
276
308
  const defaultSleep = (ms) => new Promise((resolve) => {
277
309
  setTimeout(resolve, ms);
278
310
  });
279
- function asFiniteNumber(value) {
280
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
281
- }
282
311
  function clampNumber(value, fallback, min, max) {
283
- const next = asFiniteNumber(value);
312
+ const next = Number.isFinite(value) ? value : void 0;
284
313
  if (next === void 0) return fallback;
285
314
  return Math.min(Math.max(next, min ?? Number.NEGATIVE_INFINITY), max ?? Number.POSITIVE_INFINITY);
286
315
  }
287
316
  function resolveAttemptCount(value, fallback) {
288
- return Math.max(1, Math.round(asFiniteNumber(value) ?? fallback));
317
+ return Math.max(1, Math.round(Number.isFinite(value) ? value : fallback));
289
318
  }
290
319
  function resolveRetryDelayMs(value) {
291
- const finite = value === Number.POSITIVE_INFINITY ? MAX_TIMER_TIMEOUT_MS : asFiniteNumber(value) ?? 0;
320
+ const finite = value === Number.POSITIVE_INFINITY ? MAX_TIMER_TIMEOUT_MS : Number.isFinite(value) ? value : 0;
292
321
  return Math.min(Math.max(Math.round(finite), 0), MAX_TIMER_TIMEOUT_MS);
293
322
  }
294
323
  function resolveJitterConfig(value, fallback) {
295
324
  if (value === "full") return "full";
296
- const fraction = asFiniteNumber(value);
325
+ const fraction = Number.isFinite(value) ? value : void 0;
297
326
  return fraction === void 0 ? fallback : Math.min(Math.max(fraction, 0), 1);
298
327
  }
299
328
  function resolveRetryConfig(defaults = DEFAULT_RETRY_CONFIG, overrides) {
@@ -403,7 +432,7 @@ var GatewayEventListeners = class {
403
432
  }
404
433
  };
405
434
  //#endregion
406
- //#region packages/gateway-client/src/protocol-client.ts
435
+ //#region packages/gateway-client/src/protocol-request.ts
407
436
  var GatewayProtocolRequestError = class extends Error {
408
437
  constructor(error) {
409
438
  super(error.message ?? "request failed");
@@ -415,6 +444,167 @@ var GatewayProtocolRequestError = class extends Error {
415
444
  this.retryAfterMs = error.retryAfterMs;
416
445
  }
417
446
  };
447
+ /** A local transport deadline, distinct from a Gateway's authoritative rejection. */
448
+ var GatewayProtocolRequestTimeoutError = class extends Error {
449
+ constructor(params, message = `gateway request timed out after ${params.timeoutMs}ms: ${params.method}`) {
450
+ super(message);
451
+ this.code = "CLIENT_TIMEOUT";
452
+ this.name = "GatewayProtocolRequestTimeoutError";
453
+ this.method = params.method;
454
+ this.timeoutMs = params.timeoutMs;
455
+ this.requestSent = params.requestSent;
456
+ }
457
+ };
458
+ //#endregion
459
+ //#region packages/gateway-client/src/pending-request.ts
460
+ /** Owns request deadlines, correlation, settlement, and generation-scoped IDs. */
461
+ var GatewayPendingRequests = class {
462
+ constructor(opts) {
463
+ this.opts = opts;
464
+ this.pending = /* @__PURE__ */ new Map();
465
+ this.retiredIds = /* @__PURE__ */ new Set();
466
+ this.collisionSuffix = 0;
467
+ }
468
+ get hasPending() {
469
+ return this.pending.size > 0;
470
+ }
471
+ get hasUnboundedPending() {
472
+ return [...this.pending.values()].some((pending) => pending.unbounded);
473
+ }
474
+ request(sender, method, params, options) {
475
+ let id;
476
+ try {
477
+ id = this.allocateRequestId();
478
+ } catch (error) {
479
+ return Promise.reject(error instanceof Error ? error : new Error(String(error)));
480
+ }
481
+ const requestedTimeoutMs = options?.timeoutMs === null ? void 0 : options?.timeoutMs ?? this.opts.requestTimeoutMs;
482
+ const timeoutMs = typeof requestedTimeoutMs === "number" && Number.isFinite(requestedTimeoutMs) ? resolveSafeTimeoutDelayMs(requestedTimeoutMs, { minMs: 0 }) : void 0;
483
+ return new Promise((resolve, reject) => {
484
+ let timeout;
485
+ let requestSent = false;
486
+ const pending = {
487
+ resolve: (value) => resolve(value),
488
+ reject,
489
+ expectFinal: options?.expectFinal === true,
490
+ acceptedNotified: false,
491
+ onAccepted: options?.onAccepted,
492
+ unbounded: timeoutMs === void 0,
493
+ method,
494
+ startedAtMs: this.opts.nowMs()
495
+ };
496
+ const cleanup = () => {
497
+ if (timeout !== void 0) clearTimeout(timeout);
498
+ options?.signal?.removeEventListener("abort", onAbort);
499
+ };
500
+ const retire = (errorCode) => {
501
+ if (this.pending.get(id) !== pending) return false;
502
+ this.pending.delete(id);
503
+ this.retiredIds.add(id);
504
+ cleanup();
505
+ this.finishTiming(id, pending, false, errorCode);
506
+ return true;
507
+ };
508
+ const onAbort = () => {
509
+ if (!retire("CLIENT_ABORTED")) return;
510
+ reject(this.opts.createRequestAbortError?.(method) ?? /* @__PURE__ */ new Error(`gateway request aborted for ${method}`));
511
+ };
512
+ if (options?.signal?.aborted) {
513
+ reject(this.opts.createRequestAbortError?.(method) ?? /* @__PURE__ */ new Error(`gateway request aborted for ${method}`));
514
+ return;
515
+ }
516
+ pending.cleanup = cleanup;
517
+ if (timeoutMs !== void 0) {
518
+ timeout = setTimeout(() => {
519
+ if (!retire("CLIENT_TIMEOUT")) return;
520
+ reject(this.opts.createRequestTimeoutError?.(method, timeoutMs, requestSent) ?? new GatewayProtocolRequestTimeoutError({
521
+ method,
522
+ timeoutMs,
523
+ requestSent
524
+ }));
525
+ }, timeoutMs);
526
+ timeout.unref?.();
527
+ }
528
+ options?.signal?.addEventListener("abort", onAbort, { once: true });
529
+ this.pending.set(id, pending);
530
+ try {
531
+ sender.send(JSON.stringify({
532
+ type: "req",
533
+ id,
534
+ method,
535
+ params
536
+ }));
537
+ if (this.pending.get(id) !== pending) return;
538
+ requestSent = true;
539
+ this.invoke("sent", () => options?.onSent?.());
540
+ } catch (error) {
541
+ if (retire("CLIENT_SEND_ERROR")) reject(error instanceof Error ? error : new Error(String(error)));
542
+ }
543
+ });
544
+ }
545
+ handleResponse(frame) {
546
+ const pending = this.pending.get(frame.id);
547
+ if (!pending) return;
548
+ const status = frame.payload?.status;
549
+ if (pending.expectFinal && status === "accepted") {
550
+ if (!pending.acceptedNotified) {
551
+ pending.acceptedNotified = true;
552
+ this.invoke("accepted", () => pending.onAccepted?.(frame.payload));
553
+ }
554
+ return;
555
+ }
556
+ this.pending.delete(frame.id);
557
+ pending.cleanup?.();
558
+ if (frame.ok) {
559
+ this.finishTiming(frame.id, pending, true);
560
+ pending.resolve(frame.payload);
561
+ return;
562
+ }
563
+ this.finishTiming(frame.id, pending, false, frame.error?.code);
564
+ pending.reject(this.opts.createRequestError?.(frame.error ?? {}) ?? new GatewayProtocolRequestError(frame.error ?? {}));
565
+ }
566
+ flush(error) {
567
+ for (const [id, pending] of this.pending) {
568
+ this.finishTiming(id, pending, false, "CLIENT_CLOSED");
569
+ pending.cleanup?.();
570
+ pending.reject(error);
571
+ }
572
+ this.pending.clear();
573
+ this.retiredIds.clear();
574
+ this.collisionSuffix = 0;
575
+ }
576
+ allocateRequestId() {
577
+ const id = this.opts.createRequestId();
578
+ if (!this.pending.has(id) && !this.retiredIds.has(id)) return id;
579
+ let uniqueId;
580
+ do {
581
+ this.collisionSuffix += 1;
582
+ uniqueId = `${id}:${this.collisionSuffix}`;
583
+ } while (this.pending.has(uniqueId) || this.retiredIds.has(uniqueId));
584
+ return uniqueId;
585
+ }
586
+ finishTiming(id, pending, ok, errorCode) {
587
+ const endedAtMs = this.opts.nowMs();
588
+ this.invoke("request timing", () => this.opts.onTiming?.({
589
+ id,
590
+ method: pending.method,
591
+ ok,
592
+ durationMs: Math.max(0, endedAtMs - pending.startedAtMs),
593
+ startedAtMs: pending.startedAtMs,
594
+ endedAtMs,
595
+ errorCode
596
+ }));
597
+ }
598
+ invoke(label, callback) {
599
+ try {
600
+ callback();
601
+ } catch (error) {
602
+ this.opts.onCallbackError?.(label, error);
603
+ }
604
+ }
605
+ };
606
+ //#endregion
607
+ //#region packages/gateway-client/src/protocol-client.ts
418
608
  /**
419
609
  * Browser-safe gateway wire client. Environment adapters own transport and auth
420
610
  * policy; this class owns the single socket/handshake/reconnect/frame state machine.
@@ -423,7 +613,6 @@ var GatewayProtocolClient = class {
423
613
  constructor(opts) {
424
614
  this.opts = opts;
425
615
  this.socket = null;
426
- this.pending = /* @__PURE__ */ new Map();
427
616
  this.listeners = new GatewayEventListeners();
428
617
  this.stopped = true;
429
618
  this.generation = 0;
@@ -442,18 +631,28 @@ var GatewayProtocolClient = class {
442
631
  factor: opts.reconnect.multiplier,
443
632
  jitter: 0
444
633
  });
634
+ this.requests = new GatewayPendingRequests({
635
+ createRequestId: opts.createRequestId,
636
+ createRequestError: opts.createRequestError,
637
+ createRequestTimeoutError: opts.createRequestTimeoutError,
638
+ createRequestAbortError: opts.createRequestAbortError,
639
+ requestTimeoutMs: opts.requestTimeoutMs,
640
+ nowMs: () => this.nowMs(),
641
+ onTiming: opts.onRequestTiming,
642
+ onCallbackError: opts.onCallbackError
643
+ });
445
644
  }
446
645
  get connected() {
447
646
  return this.socket?.isOpen() ?? false;
448
647
  }
449
648
  get hasPendingRequests() {
450
- return this.pending.size > 0;
649
+ return this.requests.hasPending;
451
650
  }
452
651
  get connecting() {
453
652
  return this.connectSent && !this.helloReceived;
454
653
  }
455
654
  get hasUnboundedPendingRequests() {
456
- return [...this.pending.values()].some((pending) => pending.unbounded);
655
+ return this.requests.hasUnboundedPending;
457
656
  }
458
657
  start() {
459
658
  if (this.socket || this.reconnectSignal) return;
@@ -474,68 +673,14 @@ var GatewayProtocolClient = class {
474
673
  this.socket = null;
475
674
  this.connectFailure = void 0;
476
675
  this.connectTiming = null;
477
- this.flushRequests(/* @__PURE__ */ new Error("gateway client stopped"));
676
+ this.requests.flush(/* @__PURE__ */ new Error("gateway client stopped"));
478
677
  socket?.close();
479
678
  }
480
679
  request(method, params, options) {
481
680
  const socket = this.socket;
482
681
  if (!socket?.isOpen()) return Promise.reject(/* @__PURE__ */ new Error("gateway not connected"));
483
682
  if (typeof method !== "string" || method.length === 0) return Promise.reject(/* @__PURE__ */ new Error("invalid request frame: method must be a non-empty string"));
484
- const id = this.opts.createRequestId();
485
- const timeoutMs = options?.timeoutMs === null ? void 0 : options?.timeoutMs ?? this.opts.requestTimeoutMs;
486
- return new Promise((resolve, reject) => {
487
- let timeout;
488
- const pending = {
489
- resolve: (value) => resolve(value),
490
- reject,
491
- expectFinal: options?.expectFinal === true,
492
- acceptedNotified: false,
493
- onAccepted: options?.onAccepted,
494
- unbounded: timeoutMs === void 0,
495
- method,
496
- startedAtMs: this.nowMs()
497
- };
498
- const onAbort = () => {
499
- this.pending.delete(id);
500
- if (timeout) clearTimeout(timeout);
501
- this.finishRequestTiming(id, pending, false, "CLIENT_ABORTED");
502
- reject(this.opts.createRequestAbortError?.(method) ?? /* @__PURE__ */ new Error(`gateway request aborted for ${method}`));
503
- };
504
- const cleanup = () => {
505
- if (timeout) clearTimeout(timeout);
506
- options?.signal?.removeEventListener("abort", onAbort);
507
- };
508
- if (options?.signal?.aborted) {
509
- reject(this.opts.createRequestAbortError?.(method) ?? /* @__PURE__ */ new Error(`gateway request aborted for ${method}`));
510
- return;
511
- }
512
- pending.cleanup = cleanup;
513
- if (timeoutMs !== void 0 && timeoutMs >= 0) {
514
- timeout = setTimeout(() => {
515
- this.pending.delete(id);
516
- options?.signal?.removeEventListener("abort", onAbort);
517
- this.finishRequestTiming(id, pending, false, "CLIENT_TIMEOUT");
518
- reject(this.opts.createRequestTimeoutError?.(method, timeoutMs) ?? /* @__PURE__ */ new Error(`gateway request timed out after ${timeoutMs}ms: ${method}`));
519
- }, timeoutMs);
520
- timeout.unref?.();
521
- }
522
- options?.signal?.addEventListener("abort", onAbort, { once: true });
523
- this.pending.set(id, pending);
524
- try {
525
- socket.send(JSON.stringify({
526
- type: "req",
527
- id,
528
- method,
529
- params
530
- }));
531
- this.invoke("sent", () => options?.onSent?.());
532
- } catch (error) {
533
- this.pending.delete(id);
534
- cleanup();
535
- this.finishRequestTiming(id, pending, false, "CLIENT_SEND_ERROR");
536
- reject(error instanceof Error ? error : new Error(String(error)));
537
- }
538
- });
683
+ return this.requests.request(socket, method, params, options);
539
684
  }
540
685
  addEventListener(listener) {
541
686
  return this.listeners.add(listener);
@@ -571,6 +716,7 @@ var GatewayProtocolClient = class {
571
716
  const generation = this.generation + 1;
572
717
  this.lastSeq = null;
573
718
  this.connectNonce = null;
719
+ this.connectChallengeTs = void 0;
574
720
  this.connectSent = this.connectRequestSent = false;
575
721
  this.socketOpened = false;
576
722
  this.helloReceived = false;
@@ -641,6 +787,7 @@ var GatewayProtocolClient = class {
641
787
  try {
642
788
  planOrPromise = this.opts.buildConnectPlan({
643
789
  nonce: this.connectNonce,
790
+ challengeTs: this.connectChallengeTs,
644
791
  generation
645
792
  });
646
793
  } catch (error) {
@@ -669,6 +816,7 @@ var GatewayProtocolClient = class {
669
816
  const context = {
670
817
  generation,
671
818
  nonce: this.connectNonce,
819
+ challengeTs: this.connectChallengeTs,
672
820
  plan
673
821
  };
674
822
  this.recordTiming("connect-plan-ready", generation, plan);
@@ -721,6 +869,8 @@ var GatewayProtocolClient = class {
721
869
  return;
722
870
  }
723
871
  this.connectNonce = nonce;
872
+ const challengeTs = payload?.ts;
873
+ this.connectChallengeTs = typeof challengeTs === "number" && Number.isSafeInteger(challengeTs) && challengeTs >= 0 ? challengeTs : null;
724
874
  this.recordTiming("challenge", generation);
725
875
  this.sendConnect(socket, generation);
726
876
  return;
@@ -747,28 +897,7 @@ var GatewayProtocolClient = class {
747
897
  }
748
898
  if (!isGatewayResponseFrame(parsed)) return;
749
899
  this.opts.onActivity?.();
750
- this.handleResponse(parsed);
751
- }
752
- handleResponse(frame) {
753
- const pending = this.pending.get(frame.id);
754
- if (!pending) return;
755
- const status = frame.payload?.status;
756
- if (pending.expectFinal && status === "accepted") {
757
- if (!pending.acceptedNotified) {
758
- pending.acceptedNotified = true;
759
- this.invoke("accepted", () => pending.onAccepted?.(frame.payload));
760
- }
761
- return;
762
- }
763
- this.pending.delete(frame.id);
764
- pending.cleanup?.();
765
- if (frame.ok) {
766
- this.finishRequestTiming(frame.id, pending, true);
767
- pending.resolve(frame.payload);
768
- return;
769
- }
770
- this.finishRequestTiming(frame.id, pending, false, frame.error?.code);
771
- pending.reject(this.opts.createRequestError?.(frame.error ?? {}) ?? new GatewayProtocolRequestError(frame.error ?? {}));
900
+ this.requests.handleResponse(parsed);
772
901
  }
773
902
  handleClose(socket, generation, code, reason) {
774
903
  if (this.socket !== socket) {
@@ -796,34 +925,15 @@ var GatewayProtocolClient = class {
796
925
  };
797
926
  this.connectFailure = void 0;
798
927
  const decision = this.opts.resolveClose(context);
799
- this.flushRequests(decision.pendingError ?? context.connectFailure?.error ?? /* @__PURE__ */ new Error(`gateway closed (${code}): ${reason}`));
928
+ this.requests.flush(decision.pendingError ?? context.connectFailure?.error ?? /* @__PURE__ */ new Error(`gateway closed (${code}): ${reason}`));
800
929
  this.invoke("close", () => this.opts.onClose?.(context, decision));
801
930
  if (decision.retry && !this.stopped) this.scheduleReconnect(decision.reconnectDelayMs ?? context.connectFailure?.reconnectDelayMs);
802
931
  }
803
932
  handleSocketError(socket, generation, error) {
804
933
  if (!this.isActive(socket, generation) || this.connectSent) return;
934
+ this.connectFailure = { error };
805
935
  this.opts.onConnectError?.(error);
806
936
  }
807
- flushRequests(error) {
808
- for (const [id, pending] of this.pending) {
809
- this.finishRequestTiming(id, pending, false, "CLIENT_CLOSED");
810
- pending.cleanup?.();
811
- pending.reject(error);
812
- }
813
- this.pending.clear();
814
- }
815
- finishRequestTiming(id, pending, ok, errorCode) {
816
- const endedAtMs = this.nowMs();
817
- this.invoke("request timing", () => this.opts.onRequestTiming?.({
818
- id,
819
- method: pending.method,
820
- ok,
821
- durationMs: Math.max(0, endedAtMs - pending.startedAtMs),
822
- startedAtMs: pending.startedAtMs,
823
- endedAtMs,
824
- errorCode
825
- }));
826
- }
827
937
  scheduleReconnect(overrideMs) {
828
938
  if (overrideMs !== void 0) this.reconnectSupervisor.nextDelayOverrideMs = overrideMs;
829
939
  const retry = this.reconnectSupervisor.next();
@@ -873,6 +983,7 @@ const NON_RECOVERABLE_AUTH_ERRORS = /* @__PURE__ */ new Set([
873
983
  ConnectErrorDetailCodes.AUTH_RATE_LIMITED,
874
984
  ConnectErrorDetailCodes.AUTH_DEVICE_TOKEN_MISMATCH,
875
985
  ConnectErrorDetailCodes.AUTH_SCOPE_MISMATCH,
986
+ ConnectErrorDetailCodes.CONTROL_UI_BUILD_MISMATCH,
876
987
  ConnectErrorDetailCodes.PAIRING_REQUIRED,
877
988
  ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED,
878
989
  ConnectErrorDetailCodes.DEVICE_IDENTITY_REQUIRED
@@ -886,7 +997,47 @@ function shouldPauseGatewayReconnect(params) {
886
997
  return NON_RECOVERABLE_AUTH_ERRORS.has(code) || params.protocolMismatchIsTerminal === true && code === ConnectErrorDetailCodes.PROTOCOL_MISMATCH || params.clientVersionMismatchIsTerminal === true && code === ConnectErrorDetailCodes.CLIENT_VERSION_MISMATCH;
887
998
  }
888
999
  //#endregion
1000
+ //#region packages/gateway-client/src/session-projection-run-event.ts
1001
+ function readNonemptyString$1(value) {
1002
+ return typeof value === "string" ? value.trim() || null : null;
1003
+ }
1004
+ function reduceSessionProjectionRunEventImpl(projection, event, scope = {}) {
1005
+ const runId = readNonemptyString$1(event.runId);
1006
+ if (!runId || typeof event.state !== "string" || ![
1007
+ "delta",
1008
+ "final",
1009
+ "error",
1010
+ "aborted"
1011
+ ].includes(event.state)) return null;
1012
+ const message = event.message;
1013
+ const messageStopReason = message !== null && typeof message === "object" && !Array.isArray(message) ? readNonemptyString$1(message.stopReason) : null;
1014
+ const stopReason = readNonemptyString$1(event.stopReason) ?? messageStopReason;
1015
+ const errorKind = readNonemptyString$1(event.errorKind);
1016
+ const base = {
1017
+ runId,
1018
+ ...message === void 0 ? {} : { message },
1019
+ scope
1020
+ };
1021
+ const next = reduceSessionProjection(projection, event.state === "delta" ? {
1022
+ type: "runDelta",
1023
+ ...base
1024
+ } : {
1025
+ type: "runTerminal",
1026
+ ...base,
1027
+ status: event.state === "aborted" ? "aborted" : event.state === "error" ? errorKind === "timeout" ? "timeout" : "error" : event.yielded === true && stopReason === "end_turn" ? "yielded" : stopReason === "error" ? "error" : "completed",
1028
+ ...stopReason === null ? {} : { stopReason },
1029
+ ...errorKind === null ? {} : { errorKind },
1030
+ ...typeof event.errorMessage === "string" ? { errorMessage: event.errorMessage } : {}
1031
+ });
1032
+ return {
1033
+ projection: next,
1034
+ previousRun: projection.runs[runId],
1035
+ currentRun: next.runs[runId]
1036
+ };
1037
+ }
1038
+ //#endregion
889
1039
  //#region packages/gateway-client/src/session-projection.ts
1040
+ /** Browser-safe identity and replay rules shared by Gateway conversation clients. */
890
1041
  const MAX_TRACKED_SESSION_RUNS = 200;
891
1042
  const RETAINED_SESSION_RUNS = 150;
892
1043
  const SESSION_PROJECTION_SCOPE_KEYS = [
@@ -1000,13 +1151,16 @@ function sameTranscriptIdentity(left, right) {
1000
1151
  }
1001
1152
  function entryMatches(left, right, allowSnapshotPromotion = false) {
1002
1153
  if (sameTranscriptIdentity(left.identity, right.identity)) return true;
1154
+ const durableEntry = left.identity?.id ? left : right.identity?.id ? right : null;
1155
+ const provisionalEntry = durableEntry === left ? right : durableEntry === right ? left : null;
1156
+ if (durableEntry?.live && provisionalEntry?.live && durableEntry.identity?.role === "assistant" && provisionalEntry.identity?.role === "assistant" && !durableEntry.identity.isImported && !provisionalEntry.identity.isImported && !provisionalEntry.identity.id && durableEntry.identity.runId && durableEntry.identity.runId === provisionalEntry.identity.runId) return true;
1003
1157
  const persisted = left.identity;
1004
1158
  const observed = right.identity;
1005
1159
  if (allowSnapshotPromotion && right.live && persisted && observed && persisted.role === observed.role && !persisted.isImported && !observed.isImported && persisted.id && !observed.id && (persisted.sequence !== null && persisted.sequence === observed.sequence || persisted.role === "assistant" && observed.sequence === null && persisted.runId !== null && persisted.runId === observed.runId)) return true;
1006
1160
  if (left.pending && right.pending) return Boolean(left.identity?.role === right.identity?.role && left.pendingRunId && left.pendingRunId === right.pendingRunId);
1007
1161
  const pending = left.pending ? left : right.pending ? right : null;
1008
1162
  const authoritative = pending === left ? right : pending === right ? left : null;
1009
- return Boolean(pending && authoritative && pending.identity?.role === authoritative.identity?.role && !pending.identity?.isImported && !authoritative.identity?.isImported && pending.pendingRunId && pending.pendingRunId === authoritative.identity?.runId);
1163
+ return Boolean(pending && authoritative && pending.identity && authoritative.identity && pending.identity.role === authoritative.identity.role && !pending.identity.isImported && !authoritative.identity.isImported && pending.pendingRunId && pending.pendingRunId === authoritative.identity.runId && (pending.identity.sequence === null || authoritative.identity.sequence === null || pending.identity.sequence === authoritative.identity.sequence));
1010
1164
  }
1011
1165
  function withEntries(state, entries) {
1012
1166
  return {
@@ -1188,7 +1342,13 @@ function reduceSessionProjection(state, event) {
1188
1342
  const pendingRunId = normalizeSessionProjectionRunId(event.idempotencyKey ?? event.runId);
1189
1343
  const incoming = createEntry(event.message, { pendingRunId });
1190
1344
  if (!pendingRunId || !incoming.identity) return state;
1191
- return state.entries.findIndex((entry) => entryMatches(entry, incoming)) < 0 ? withEntries(state, insertEntry(state.entries, incoming, state.runs)) : state;
1345
+ const seed = state.entries.find((entry) => entry.message === event.message);
1346
+ if (seed && !seed.pending && incoming.identity.id === null && !incoming.identity.isImported && incoming.identity.runId === pendingRunId) return withEntries(state, state.entries.map((entry) => entry === seed ? {
1347
+ ...seed,
1348
+ pending: true,
1349
+ pendingRunId
1350
+ } : entry));
1351
+ return seed || state.entries.some((entry) => entryMatches(entry, incoming)) ? state : withEntries(state, insertEntry(state.entries, incoming, state.runs));
1192
1352
  }
1193
1353
  case "sendAcknowledged": {
1194
1354
  const runId = normalizeSessionProjectionRunId(event.idempotencyKey ?? event.runId);
@@ -1234,38 +1394,7 @@ function reduceSessionProjection(state, event) {
1234
1394
  }
1235
1395
  /** Normalizes Gateway run envelopes once for every browser and terminal adapter. */
1236
1396
  function reduceSessionProjectionRunEvent(projection, event, scope = {}) {
1237
- const runId = readNonemptyString(event.runId);
1238
- const eventState = event.state;
1239
- if (!runId || typeof eventState !== "string" || ![
1240
- "delta",
1241
- "final",
1242
- "error",
1243
- "aborted"
1244
- ].includes(eventState)) return null;
1245
- const message = event.message;
1246
- const stopReason = readNonemptyString(event.stopReason) ?? readNonemptyString(readRecord(message)?.stopReason);
1247
- const errorKind = readNonemptyString(event.errorKind);
1248
- const base = {
1249
- runId,
1250
- ...message === void 0 ? {} : { message },
1251
- scope
1252
- };
1253
- const next = reduceSessionProjection(projection, eventState === "delta" ? {
1254
- type: "runDelta",
1255
- ...base
1256
- } : {
1257
- type: "runTerminal",
1258
- ...base,
1259
- status: eventState === "aborted" ? "aborted" : eventState === "error" ? errorKind === "timeout" ? "timeout" : "error" : event.yielded === true && stopReason === "end_turn" ? "yielded" : stopReason === "error" ? "error" : "completed",
1260
- ...stopReason === null ? {} : { stopReason },
1261
- ...errorKind === null ? {} : { errorKind },
1262
- ...typeof event.errorMessage === "string" ? { errorMessage: event.errorMessage } : {}
1263
- });
1264
- return {
1265
- projection: next,
1266
- previousRun: projection.runs[runId],
1267
- currentRun: next.runs[runId]
1268
- };
1397
+ return reduceSessionProjectionRunEventImpl(projection, event, scope);
1269
1398
  }
1270
1399
  //#endregion
1271
1400
  //#region packages/gateway-client/src/session-subscriptions.ts
@@ -1473,4 +1602,4 @@ function releaseGatewaySessionMessageSubscription(subscription) {
1473
1602
  return sessionMessageSubscriptionOwners.get(subscription)?.coordinator.release(subscription) ?? Promise.resolve();
1474
1603
  }
1475
1604
  //#endregion
1476
- export { buildDeviceAuthPayload as C, shouldRetryGatewayWithDeviceToken as S, normalizeDeviceMetadataForAuth as T, GatewayProtocolRequestError as _, createSessionProjection as a, resolveGatewayConnectScopes as b, normalizeSessionProjectionRunId as c, readSessionMessageSequence as d, reconcileSessionProjectionSnapshot as f, GatewayProtocolClient as g, shouldPauseGatewayReconnect as h, resetGatewaySessionMessageSubscriptionCoordinator as i, projectLiveSessionMessage as l, reduceSessionProjectionRunEvent as m, getGatewaySessionMessageSubscriptionCoordinator as n, hasSessionProjectionAcceptedFinal as o, reduceSessionProjection as p, releaseGatewaySessionMessageSubscription as r, isLocallyOptimisticSessionMessage as s, GatewaySessionMessageSubscriptionCoordinator as t, readSessionMessageIdentity as u, GatewayBrowserDeviceAuthLifecycle as v, buildDeviceAuthPayloadV3 as w, selectGatewayConnectAuth as x, buildGatewayConnectAuth as y };
1605
+ export { resolveGatewayConnectScopes as C, buildDeviceAuthPayloadV3 as D, buildDeviceAuthPayload as E, normalizeDeviceMetadataForAuth as O, buildGatewayConnectAuth as S, shouldRetryGatewayWithDeviceToken as T, GatewayProtocolRequestError as _, createSessionProjection as a, gatewayOriginScope as b, normalizeSessionProjectionRunId as c, readSessionMessageSequence as d, reconcileSessionProjectionSnapshot as f, GatewayProtocolClient as g, shouldPauseGatewayReconnect as h, resetGatewaySessionMessageSubscriptionCoordinator as i, projectLiveSessionMessage as l, reduceSessionProjectionRunEvent as m, getGatewaySessionMessageSubscriptionCoordinator as n, hasSessionProjectionAcceptedFinal as o, reduceSessionProjection as p, releaseGatewaySessionMessageSubscription as r, isLocallyOptimisticSessionMessage as s, GatewaySessionMessageSubscriptionCoordinator as t, readSessionMessageIdentity as u, GatewayProtocolRequestTimeoutError as v, selectGatewayConnectAuth as w, GatewayBrowserDeviceAuthLifecycle as x, gatewayCredentialScope as y };
package/dist/timeouts.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region packages/gateway-client/src/timeouts.ts
2
- function parseStrictPositiveInteger(value) {
2
+ function parsePositiveTimeoutSetting(value) {
3
3
  const trimmed = value.trim();
4
4
  if (!/^\+?\d+$/u.test(trimmed)) return;
5
5
  const parsed = Number(trimmed);
@@ -53,7 +53,7 @@ function clampConnectChallengeTimeoutMs(timeoutMs, maxTimeoutMs = MAX_CONNECT_CH
53
53
  function getConnectChallengeTimeoutMsFromEnv(env = process.env) {
54
54
  const raw = env.OPENCLAW_CONNECT_CHALLENGE_TIMEOUT_MS;
55
55
  if (raw) {
56
- const parsed = parseStrictPositiveInteger(raw);
56
+ const parsed = parsePositiveTimeoutSetting(raw);
57
57
  if (parsed !== void 0) return resolveSafeTimeoutDelayMs(parsed);
58
58
  }
59
59
  }
@@ -77,7 +77,7 @@ function resolvePreauthHandshakeTimeoutMs(params) {
77
77
  const env = params?.env ?? process.env;
78
78
  const configuredTimeout = env.OPENCLAW_HANDSHAKE_TIMEOUT_MS || (isTestRuntimeEnv(env) ? env.OPENCLAW_TEST_HANDSHAKE_TIMEOUT_MS : void 0);
79
79
  if (configuredTimeout) {
80
- const parsed = parseStrictPositiveInteger(configuredTimeout);
80
+ const parsed = parsePositiveTimeoutSetting(configuredTimeout);
81
81
  if (parsed !== void 0) return resolveSafeTimeoutDelayMs(parsed);
82
82
  }
83
83
  const configured = normalizePositiveTimeoutMs(params?.configuredTimeoutMs);
@@ -0,0 +1,6 @@
1
+ import { RawData } from "ws";
2
+
3
+ //#region packages/gateway-client/src/websocket-data.d.ts
4
+ declare function rawDataToString(data: RawData, encoding?: BufferEncoding): string;
5
+ //#endregion
6
+ export { rawDataToString };
@@ -0,0 +1,8 @@
1
+ import { Buffer } from "node:buffer";
2
+ //#region packages/gateway-client/src/websocket-data.ts
3
+ function rawDataToString(data, encoding = "utf8") {
4
+ if (Array.isArray(data)) return Buffer.concat(data).toString(encoding);
5
+ return data instanceof ArrayBuffer ? Buffer.from(data).toString(encoding) : data.toString(encoding);
6
+ }
7
+ //#endregion
8
+ export { rawDataToString };