@openclaw/gateway-client 2026.8.1-beta.1 → 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);
@@ -177,12 +177,18 @@ var GatewayBrowserDeviceAuthLifecycle = class {
177
177
  async acceptHello(hello, plan) {
178
178
  const token = hello.auth?.deviceToken?.trim();
179
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
+ });
180
186
  await this.deps.tokenStore.store({
181
187
  clientId: plan.clientId,
182
188
  deviceId: plan.identity.deviceId,
183
- role: hello.auth?.role ?? plan.role,
189
+ role,
184
190
  token,
185
- scopes: hello.auth?.scopes ?? []
191
+ scopes: stored?.token === token ? stored.scopes : hello.auth?.scopes ?? []
186
192
  });
187
193
  }
188
194
  async clearStoredToken(plan) {
@@ -195,6 +201,29 @@ var GatewayBrowserDeviceAuthLifecycle = class {
195
201
  }
196
202
  };
197
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
198
227
  //#region packages/retry/src/index.ts
199
228
  const MAX_TIMER_TIMEOUT_MS = 2147e6;
200
229
  function computeBackoff(policy, attempt) {
@@ -215,7 +244,9 @@ async function sleepWithAbort(ms, abortSignal, options = {}) {
215
244
  if (timer) clearTimeout(timer);
216
245
  timer = null;
217
246
  cleanup();
218
- 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);
219
250
  };
220
251
  abortSignal?.addEventListener("abort", onAbort, { once: true });
221
252
  if (abortSignal?.aborted) {
@@ -277,24 +308,21 @@ const DEFAULT_RETRY_CONFIG = {
277
308
  const defaultSleep = (ms) => new Promise((resolve) => {
278
309
  setTimeout(resolve, ms);
279
310
  });
280
- function asFiniteNumber(value) {
281
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
282
- }
283
311
  function clampNumber(value, fallback, min, max) {
284
- const next = asFiniteNumber(value);
312
+ const next = Number.isFinite(value) ? value : void 0;
285
313
  if (next === void 0) return fallback;
286
314
  return Math.min(Math.max(next, min ?? Number.NEGATIVE_INFINITY), max ?? Number.POSITIVE_INFINITY);
287
315
  }
288
316
  function resolveAttemptCount(value, fallback) {
289
- return Math.max(1, Math.round(asFiniteNumber(value) ?? fallback));
317
+ return Math.max(1, Math.round(Number.isFinite(value) ? value : fallback));
290
318
  }
291
319
  function resolveRetryDelayMs(value) {
292
- 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;
293
321
  return Math.min(Math.max(Math.round(finite), 0), MAX_TIMER_TIMEOUT_MS);
294
322
  }
295
323
  function resolveJitterConfig(value, fallback) {
296
324
  if (value === "full") return "full";
297
- const fraction = asFiniteNumber(value);
325
+ const fraction = Number.isFinite(value) ? value : void 0;
298
326
  return fraction === void 0 ? fallback : Math.min(Math.max(fraction, 0), 1);
299
327
  }
300
328
  function resolveRetryConfig(defaults = DEFAULT_RETRY_CONFIG, overrides) {
@@ -416,6 +444,165 @@ var GatewayProtocolRequestError = class extends Error {
416
444
  this.retryAfterMs = error.retryAfterMs;
417
445
  }
418
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
+ };
419
606
  //#endregion
420
607
  //#region packages/gateway-client/src/protocol-client.ts
421
608
  /**
@@ -426,7 +613,6 @@ var GatewayProtocolClient = class {
426
613
  constructor(opts) {
427
614
  this.opts = opts;
428
615
  this.socket = null;
429
- this.pending = /* @__PURE__ */ new Map();
430
616
  this.listeners = new GatewayEventListeners();
431
617
  this.stopped = true;
432
618
  this.generation = 0;
@@ -445,18 +631,28 @@ var GatewayProtocolClient = class {
445
631
  factor: opts.reconnect.multiplier,
446
632
  jitter: 0
447
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
+ });
448
644
  }
449
645
  get connected() {
450
646
  return this.socket?.isOpen() ?? false;
451
647
  }
452
648
  get hasPendingRequests() {
453
- return this.pending.size > 0;
649
+ return this.requests.hasPending;
454
650
  }
455
651
  get connecting() {
456
652
  return this.connectSent && !this.helloReceived;
457
653
  }
458
654
  get hasUnboundedPendingRequests() {
459
- return [...this.pending.values()].some((pending) => pending.unbounded);
655
+ return this.requests.hasUnboundedPending;
460
656
  }
461
657
  start() {
462
658
  if (this.socket || this.reconnectSignal) return;
@@ -477,71 +673,14 @@ var GatewayProtocolClient = class {
477
673
  this.socket = null;
478
674
  this.connectFailure = void 0;
479
675
  this.connectTiming = null;
480
- this.flushRequests(/* @__PURE__ */ new Error("gateway client stopped"));
676
+ this.requests.flush(/* @__PURE__ */ new Error("gateway client stopped"));
481
677
  socket?.close();
482
678
  }
483
679
  request(method, params, options) {
484
680
  const socket = this.socket;
485
681
  if (!socket?.isOpen()) return Promise.reject(/* @__PURE__ */ new Error("gateway not connected"));
486
682
  if (typeof method !== "string" || method.length === 0) return Promise.reject(/* @__PURE__ */ new Error("invalid request frame: method must be a non-empty string"));
487
- const id = this.opts.createRequestId();
488
- const timeoutMs = options?.timeoutMs === null ? void 0 : options?.timeoutMs ?? this.opts.requestTimeoutMs;
489
- return new Promise((resolve, reject) => {
490
- let timeout;
491
- let requestSent = false;
492
- const pending = {
493
- resolve: (value) => resolve(value),
494
- reject,
495
- expectFinal: options?.expectFinal === true,
496
- acceptedNotified: false,
497
- onAccepted: options?.onAccepted,
498
- unbounded: timeoutMs === void 0,
499
- method,
500
- startedAtMs: this.nowMs()
501
- };
502
- const onAbort = () => {
503
- this.pending.delete(id);
504
- pending.cleanup?.();
505
- this.finishRequestTiming(id, pending, false, "CLIENT_ABORTED");
506
- reject(this.opts.createRequestAbortError?.(method) ?? /* @__PURE__ */ new Error(`gateway request aborted for ${method}`));
507
- };
508
- const cleanup = () => {
509
- if (timeout) clearTimeout(timeout);
510
- options?.signal?.removeEventListener("abort", onAbort);
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 && timeoutMs >= 0) {
518
- timeout = setTimeout(() => {
519
- if (this.pending.get(id) !== pending) return;
520
- this.pending.delete(id);
521
- options?.signal?.removeEventListener("abort", onAbort);
522
- this.finishRequestTiming(id, pending, false, "CLIENT_TIMEOUT");
523
- reject(this.opts.createRequestTimeoutError?.(method, timeoutMs, requestSent) ?? /* @__PURE__ */ new Error(`gateway request timed out after ${timeoutMs}ms: ${method}`));
524
- }, timeoutMs);
525
- timeout.unref?.();
526
- }
527
- options?.signal?.addEventListener("abort", onAbort, { once: true });
528
- this.pending.set(id, pending);
529
- try {
530
- socket.send(JSON.stringify({
531
- type: "req",
532
- id,
533
- method,
534
- params
535
- }));
536
- requestSent = true;
537
- this.invoke("sent", () => options?.onSent?.());
538
- } catch (error) {
539
- this.pending.delete(id);
540
- cleanup();
541
- this.finishRequestTiming(id, pending, false, "CLIENT_SEND_ERROR");
542
- reject(error instanceof Error ? error : new Error(String(error)));
543
- }
544
- });
683
+ return this.requests.request(socket, method, params, options);
545
684
  }
546
685
  addEventListener(listener) {
547
686
  return this.listeners.add(listener);
@@ -758,28 +897,7 @@ var GatewayProtocolClient = class {
758
897
  }
759
898
  if (!isGatewayResponseFrame(parsed)) return;
760
899
  this.opts.onActivity?.();
761
- this.handleResponse(parsed);
762
- }
763
- handleResponse(frame) {
764
- const pending = this.pending.get(frame.id);
765
- if (!pending) return;
766
- const status = frame.payload?.status;
767
- if (pending.expectFinal && status === "accepted") {
768
- if (!pending.acceptedNotified) {
769
- pending.acceptedNotified = true;
770
- this.invoke("accepted", () => pending.onAccepted?.(frame.payload));
771
- }
772
- return;
773
- }
774
- this.pending.delete(frame.id);
775
- pending.cleanup?.();
776
- if (frame.ok) {
777
- this.finishRequestTiming(frame.id, pending, true);
778
- pending.resolve(frame.payload);
779
- return;
780
- }
781
- this.finishRequestTiming(frame.id, pending, false, frame.error?.code);
782
- pending.reject(this.opts.createRequestError?.(frame.error ?? {}) ?? new GatewayProtocolRequestError(frame.error ?? {}));
900
+ this.requests.handleResponse(parsed);
783
901
  }
784
902
  handleClose(socket, generation, code, reason) {
785
903
  if (this.socket !== socket) {
@@ -807,34 +925,15 @@ var GatewayProtocolClient = class {
807
925
  };
808
926
  this.connectFailure = void 0;
809
927
  const decision = this.opts.resolveClose(context);
810
- 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}`));
811
929
  this.invoke("close", () => this.opts.onClose?.(context, decision));
812
930
  if (decision.retry && !this.stopped) this.scheduleReconnect(decision.reconnectDelayMs ?? context.connectFailure?.reconnectDelayMs);
813
931
  }
814
932
  handleSocketError(socket, generation, error) {
815
933
  if (!this.isActive(socket, generation) || this.connectSent) return;
934
+ this.connectFailure = { error };
816
935
  this.opts.onConnectError?.(error);
817
936
  }
818
- flushRequests(error) {
819
- for (const [id, pending] of this.pending) {
820
- this.finishRequestTiming(id, pending, false, "CLIENT_CLOSED");
821
- pending.cleanup?.();
822
- pending.reject(error);
823
- }
824
- this.pending.clear();
825
- }
826
- finishRequestTiming(id, pending, ok, errorCode) {
827
- const endedAtMs = this.nowMs();
828
- this.invoke("request timing", () => this.opts.onRequestTiming?.({
829
- id,
830
- method: pending.method,
831
- ok,
832
- durationMs: Math.max(0, endedAtMs - pending.startedAtMs),
833
- startedAtMs: pending.startedAtMs,
834
- endedAtMs,
835
- errorCode
836
- }));
837
- }
838
937
  scheduleReconnect(overrideMs) {
839
938
  if (overrideMs !== void 0) this.reconnectSupervisor.nextDelayOverrideMs = overrideMs;
840
939
  const retry = this.reconnectSupervisor.next();
@@ -884,6 +983,7 @@ const NON_RECOVERABLE_AUTH_ERRORS = /* @__PURE__ */ new Set([
884
983
  ConnectErrorDetailCodes.AUTH_RATE_LIMITED,
885
984
  ConnectErrorDetailCodes.AUTH_DEVICE_TOKEN_MISMATCH,
886
985
  ConnectErrorDetailCodes.AUTH_SCOPE_MISMATCH,
986
+ ConnectErrorDetailCodes.CONTROL_UI_BUILD_MISMATCH,
887
987
  ConnectErrorDetailCodes.PAIRING_REQUIRED,
888
988
  ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED,
889
989
  ConnectErrorDetailCodes.DEVICE_IDENTITY_REQUIRED
@@ -897,7 +997,47 @@ function shouldPauseGatewayReconnect(params) {
897
997
  return NON_RECOVERABLE_AUTH_ERRORS.has(code) || params.protocolMismatchIsTerminal === true && code === ConnectErrorDetailCodes.PROTOCOL_MISMATCH || params.clientVersionMismatchIsTerminal === true && code === ConnectErrorDetailCodes.CLIENT_VERSION_MISMATCH;
898
998
  }
899
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
900
1039
  //#region packages/gateway-client/src/session-projection.ts
1040
+ /** Browser-safe identity and replay rules shared by Gateway conversation clients. */
901
1041
  const MAX_TRACKED_SESSION_RUNS = 200;
902
1042
  const RETAINED_SESSION_RUNS = 150;
903
1043
  const SESSION_PROJECTION_SCOPE_KEYS = [
@@ -1011,13 +1151,16 @@ function sameTranscriptIdentity(left, right) {
1011
1151
  }
1012
1152
  function entryMatches(left, right, allowSnapshotPromotion = false) {
1013
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;
1014
1157
  const persisted = left.identity;
1015
1158
  const observed = right.identity;
1016
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;
1017
1160
  if (left.pending && right.pending) return Boolean(left.identity?.role === right.identity?.role && left.pendingRunId && left.pendingRunId === right.pendingRunId);
1018
1161
  const pending = left.pending ? left : right.pending ? right : null;
1019
1162
  const authoritative = pending === left ? right : pending === right ? left : null;
1020
- 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));
1021
1164
  }
1022
1165
  function withEntries(state, entries) {
1023
1166
  return {
@@ -1199,7 +1342,13 @@ function reduceSessionProjection(state, event) {
1199
1342
  const pendingRunId = normalizeSessionProjectionRunId(event.idempotencyKey ?? event.runId);
1200
1343
  const incoming = createEntry(event.message, { pendingRunId });
1201
1344
  if (!pendingRunId || !incoming.identity) return state;
1202
- 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));
1203
1352
  }
1204
1353
  case "sendAcknowledged": {
1205
1354
  const runId = normalizeSessionProjectionRunId(event.idempotencyKey ?? event.runId);
@@ -1245,38 +1394,7 @@ function reduceSessionProjection(state, event) {
1245
1394
  }
1246
1395
  /** Normalizes Gateway run envelopes once for every browser and terminal adapter. */
1247
1396
  function reduceSessionProjectionRunEvent(projection, event, scope = {}) {
1248
- const runId = readNonemptyString(event.runId);
1249
- const eventState = event.state;
1250
- if (!runId || typeof eventState !== "string" || ![
1251
- "delta",
1252
- "final",
1253
- "error",
1254
- "aborted"
1255
- ].includes(eventState)) return null;
1256
- const message = event.message;
1257
- const stopReason = readNonemptyString(event.stopReason) ?? readNonemptyString(readRecord(message)?.stopReason);
1258
- const errorKind = readNonemptyString(event.errorKind);
1259
- const base = {
1260
- runId,
1261
- ...message === void 0 ? {} : { message },
1262
- scope
1263
- };
1264
- const next = reduceSessionProjection(projection, eventState === "delta" ? {
1265
- type: "runDelta",
1266
- ...base
1267
- } : {
1268
- type: "runTerminal",
1269
- ...base,
1270
- status: eventState === "aborted" ? "aborted" : eventState === "error" ? errorKind === "timeout" ? "timeout" : "error" : event.yielded === true && stopReason === "end_turn" ? "yielded" : stopReason === "error" ? "error" : "completed",
1271
- ...stopReason === null ? {} : { stopReason },
1272
- ...errorKind === null ? {} : { errorKind },
1273
- ...typeof event.errorMessage === "string" ? { errorMessage: event.errorMessage } : {}
1274
- });
1275
- return {
1276
- projection: next,
1277
- previousRun: projection.runs[runId],
1278
- currentRun: next.runs[runId]
1279
- };
1397
+ return reduceSessionProjectionRunEventImpl(projection, event, scope);
1280
1398
  }
1281
1399
  //#endregion
1282
1400
  //#region packages/gateway-client/src/session-subscriptions.ts
@@ -1484,4 +1602,4 @@ function releaseGatewaySessionMessageSubscription(subscription) {
1484
1602
  return sessionMessageSubscriptionOwners.get(subscription)?.coordinator.release(subscription) ?? Promise.resolve();
1485
1603
  }
1486
1604
  //#endregion
1487
- 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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/gateway-client",
3
- "version": "2026.8.1-beta.1",
3
+ "version": "2026.8.1-beta.2",
4
4
  "description": "Reference WebSocket client for the OpenClaw Gateway protocol",
5
5
  "keywords": [
6
6
  "client",
@@ -45,16 +45,26 @@
45
45
  "import": "./dist/readiness.mjs",
46
46
  "default": "./dist/readiness.mjs"
47
47
  },
48
+ "./scope-upgrade": {
49
+ "types": "./dist/scope-upgrade.d.mts",
50
+ "import": "./dist/scope-upgrade.mjs",
51
+ "default": "./dist/scope-upgrade.mjs"
52
+ },
48
53
  "./timeouts": {
49
54
  "types": "./dist/timeouts.d.mts",
50
55
  "import": "./dist/timeouts.mjs",
51
56
  "default": "./dist/timeouts.mjs"
57
+ },
58
+ "./websocket-data": {
59
+ "types": "./dist/websocket-data.d.mts",
60
+ "import": "./dist/websocket-data.mjs",
61
+ "default": "./dist/websocket-data.mjs"
52
62
  }
53
63
  },
54
64
  "dependencies": {
55
65
  "ipaddr.js": "2.4.0",
56
66
  "ws": "8.21.1",
57
- "@openclaw/gateway-protocol": "2026.8.1-beta.1"
67
+ "@openclaw/gateway-protocol": "2026.8.1-beta.2"
58
68
  },
59
69
  "engines": {
60
70
  "node": ">=22.19.0"
@@ -68,6 +78,6 @@
68
78
  }
69
79
  },
70
80
  "scripts": {
71
- "build": "tsdown src/index.ts src/browser.ts src/readiness.ts src/timeouts.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
81
+ "build": "tsdown src/index.ts src/browser.ts src/readiness.ts src/scope-upgrade.ts src/timeouts.ts src/websocket-data.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
72
82
  }
73
83
  }