@openclaw/gateway-client 2026.8.1-beta.2 → 2026.9.1-beta.1

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,6 +1,5 @@
1
- import { n as GatewayProtocolRequestOptions, t as GatewayProtocolRequestError } from "./protocol-request-D4mVhLxE.mjs";
1
+ import { n as GatewayProtocolRequestOptions, t as GatewayProtocolRequestError } from "./protocol-request-r0jkQ1XE.mjs";
2
2
  import { ErrorShape, EventFrame, HelloOk } from "@openclaw/gateway-protocol";
3
-
4
3
  //#region packages/gateway-client/src/pending-request.d.ts
5
4
  type GatewayProtocolRequestTiming = {
6
5
  id: string;
@@ -86,6 +85,7 @@ type GatewayProtocolClientOptions<TPlan> = {
86
85
  notifyStoppedClose?: boolean;
87
86
  onConnectError?: (error: Error) => void;
88
87
  onSocketFactoryError?: (error: Error) => void;
88
+ onReconnectStopped?: (error: Error) => void;
89
89
  onParseError?: (error: unknown) => void;
90
90
  onEvent?: (event: EventFrame) => void;
91
91
  onGap?: (info: {
@@ -1,5 +1,4 @@
1
1
  import { ErrorShape } from "@openclaw/gateway-protocol";
2
-
3
2
  //#region packages/gateway-client/src/protocol-request.d.ts
4
3
  type GatewayProtocolRequestOptions = {
5
4
  timeoutMs?: number | null;
@@ -1,7 +1,7 @@
1
- import { n as GatewayProtocolRequestOptions, r as GatewayProtocolRequestTimeoutError, t as GatewayProtocolRequestError } from "./protocol-request-D4mVhLxE.mjs";
1
+ import { n as GatewayProtocolRequestOptions, r as GatewayProtocolRequestTimeoutError, t as GatewayProtocolRequestError } from "./protocol-request-r0jkQ1XE.mjs";
2
+ import "./protocol-client-C-viHJgO.mjs";
2
3
  import { ConnectParams, ErrorShape, EventFrame, HelloOk } from "@openclaw/gateway-protocol/frame-guards";
3
4
  import { GatewayClientMode, GatewayClientName } from "@openclaw/gateway-protocol/client-info";
4
-
5
5
  //#region packages/gateway-client/src/request-error.d.ts
6
6
  declare class GatewayClientRequestError extends GatewayProtocolRequestError {
7
7
  constructor(error: Partial<ErrorShape>);
@@ -70,6 +70,8 @@ declare function isGatewayConnectAssemblyError(value: unknown): value is Error;
70
70
  type GatewayClientOptions = {
71
71
  url?: string;
72
72
  origin?: string;
73
+ /** Already-resolved edge-proxy auth headers (identity-aware proxy in front of the Gateway). */
74
+ edgeAuthHeaders?: Readonly<Record<string, string>>;
73
75
  connectChallengeTimeoutMs?: number;
74
76
  /**
75
77
  * Server-side pre-auth handshake budget. Config-derived local clients use
@@ -80,7 +82,8 @@ type GatewayClientOptions = {
80
82
  tickWatchTimeoutMs?: number;
81
83
  requestTimeoutMs?: number;
82
84
  token?: string;
83
- bootstrapToken?: string; /** Prefer one setup credential for the first successful device-auth exchange. */
85
+ bootstrapToken?: string;
86
+ /** Prefer one setup credential for the first successful device-auth exchange. */
84
87
  preferBootstrapToken?: boolean;
85
88
  deviceToken?: string;
86
89
  password?: string;
@@ -99,6 +102,7 @@ type GatewayClientOptions = {
99
102
  caps?: string[];
100
103
  commands?: string[];
101
104
  computerUse?: ConnectParams["computerUse"];
105
+ /** @deprecated Compatibility for the shipped v1 node-host connect envelope. */
102
106
  workerRuns?: ConnectParams["workerRuns"];
103
107
  permissions?: Record<string, boolean>;
104
108
  pathEnv?: string;
@@ -112,6 +116,8 @@ type GatewayClientOptions = {
112
116
  onHelloOk?: (hello: HelloOk) => void;
113
117
  onConnectError?: (err: Error) => void;
114
118
  onReconnectPaused?: (info: GatewayReconnectPausedInfo) => void;
119
+ /** Report retryable startup closes for clients that present connection progress. */
120
+ notifyOnStartupRetry?: boolean;
115
121
  onClose?: (code: number, reason: string, info?: GatewayClientCloseInfo) => void;
116
122
  onGap?: (info: {
117
123
  expected: number;
@@ -150,6 +156,7 @@ declare class GatewayClient {
150
156
  caps: string[];
151
157
  commands: string[];
152
158
  computerUse?: ConnectParams["computerUse"];
159
+ /** @deprecated Compatibility for the shipped v1 node-host connect envelope. */
153
160
  workerRuns?: ConnectParams["workerRuns"];
154
161
  }): void;
155
162
  start(): void;
@@ -1,2 +1,2 @@
1
- import { a as startGatewayClientWithReadinessWait, i as startGatewayClientWhenEventLoopReady, n as GatewayClientStartReadinessOptions, r as GatewayClientStartable, t as EventLoopReadyWaiter } from "./readiness--KX7i8Mq.mjs";
1
+ import { a as startGatewayClientWithReadinessWait, i as startGatewayClientWhenEventLoopReady, n as GatewayClientStartReadinessOptions, r as GatewayClientStartable, t as EventLoopReadyWaiter } from "./readiness-CJ1QsCtR.mjs";
2
2
  export { EventLoopReadyWaiter, GatewayClientStartReadinessOptions, GatewayClientStartable, startGatewayClientWhenEventLoopReady, startGatewayClientWithReadinessWait };
@@ -1,6 +1,5 @@
1
- import { a as GatewayBrowserDeviceTokenStore } from "./browser-device-auth-Dz6H7eXy.mjs";
2
- import { n as GatewayProtocolRequestOptions } from "./protocol-request-D4mVhLxE.mjs";
3
-
1
+ import { a as GatewayBrowserDeviceTokenStore } from "./browser-device-auth-DoPo-sPB.mjs";
2
+ import { n as GatewayProtocolRequestOptions } from "./protocol-request-r0jkQ1XE.mjs";
4
3
  //#region packages/gateway-client/src/scope-upgrade.d.ts
5
4
  type ScopeUpgradeBinding = {
6
5
  clientId: string;
@@ -1,3 +1,4 @@
1
+ import { n as GatewayProtocolRequestOptions } from "./protocol-request-r0jkQ1XE.mjs";
1
2
  //#region packages/gateway-client/src/device-auth.d.ts
2
3
  declare function normalizeDeviceMetadataForAuth(value?: string | null): string;
3
4
  type DeviceAuthPayloadParams = {
@@ -23,8 +24,7 @@ declare function gatewayOriginScope(gatewayUrl: string): string;
23
24
  /** Normalizes the gateway URL scope used for browser credential records. */
24
25
  declare function gatewayCredentialScope(gatewayUrl: string): string;
25
26
  //#endregion
26
- //#region packages/gateway-client/src/session-projection.d.ts
27
- /** Browser-safe identity and replay rules shared by Gateway conversation clients. */
27
+ //#region packages/gateway-client/src/session-projection-message-identity.d.ts
28
28
  type SessionMessageEnvelope = {
29
29
  messageId?: unknown;
30
30
  messageSeq?: unknown;
@@ -41,6 +41,14 @@ type SessionMessageIdentity = {
41
41
  isImported: boolean;
42
42
  externalSource: string | null;
43
43
  };
44
+ /** History and status markers carry transcript order even when they have no chat role. */
45
+ declare function readSessionMessageSequence(message: unknown, envelope?: SessionMessageEnvelope): number | null;
46
+ /** Run ownership normalizes a user-turn suffix without changing its persisted send key. */
47
+ declare function normalizeSessionProjectionRunId(value: unknown): string | null;
48
+ /** Persisted row facts win; assistant run ownership comes from its authoritative producer. */
49
+ declare function readSessionMessageIdentity(message: unknown, envelope?: SessionMessageEnvelope): SessionMessageIdentity | null;
50
+ //#endregion
51
+ //#region packages/gateway-client/src/session-projection.d.ts
44
52
  type SessionProjectionScope = {
45
53
  sessionKey?: string;
46
54
  sessionId?: string;
@@ -122,12 +130,6 @@ type SessionProjectionEvent = ScopedSessionProjectionEvent & ({
122
130
  } | {
123
131
  type: "reconnected";
124
132
  });
125
- /** History and status markers carry transcript order even when they have no chat role. */
126
- declare function readSessionMessageSequence(message: unknown, envelope?: SessionMessageEnvelope): number | null;
127
- /** Run ownership normalizes a user-turn suffix without changing its persisted send key. */
128
- declare function normalizeSessionProjectionRunId(value: unknown): string | null;
129
- /** Persisted transcript facts win over envelope projections and provider-local import IDs. */
130
- declare function readSessionMessageIdentity(message: unknown, envelope?: SessionMessageEnvelope): SessionMessageIdentity | null;
131
133
  /** Local turns have no durable transcript metadata beyond their own optional send key. */
132
134
  declare function isLocallyOptimisticSessionMessage(message: unknown): boolean;
133
135
  declare function createSessionProjection(scope?: SessionProjectionScope, messages?: readonly unknown[]): SessionProjectionState;
@@ -143,7 +145,7 @@ declare function reduceSessionProjectionRunEvent(projection: SessionProjectionSt
143
145
  //#endregion
144
146
  //#region packages/gateway-client/src/session-subscriptions.d.ts
145
147
  type GatewaySessionMessageRequestClient = {
146
- request<T = unknown>(method: string, params: Record<string, unknown>): Promise<T>;
148
+ request<T = unknown>(method: string, params: Record<string, unknown>, options?: GatewayProtocolRequestOptions): Promise<T>;
147
149
  };
148
150
  type GatewaySessionMessageSubscription = {
149
151
  key: string;
@@ -175,4 +177,4 @@ declare function getGatewaySessionMessageSubscriptionCoordinator(client: Gateway
175
177
  declare function resetGatewaySessionMessageSubscriptionCoordinator(client: GatewaySessionMessageRequestClient): void;
176
178
  declare function releaseGatewaySessionMessageSubscription(subscription: GatewaySessionMessageSubscription): Promise<void>;
177
179
  //#endregion
178
- export { gatewayCredentialScope as A, normalizeSessionProjectionRunId as C, reconcileSessionProjectionSnapshot as D, readSessionMessageSequence as E, buildDeviceAuthPayload as M, buildDeviceAuthPayloadV3 as N, reduceSessionProjection as O, normalizeDeviceMetadataForAuth as P, isLocallyOptimisticSessionMessage as S, readSessionMessageIdentity as T, SessionProjectionScope as _, GatewaySessionMessageSubscriptionOptions as a, createSessionProjection as b, resetGatewaySessionMessageSubscriptionCoordinator as c, SessionProjectionEntry as d, SessionProjectionEvent as f, SessionProjectionRunTransition as g, SessionProjectionRunStatus as h, GatewaySessionMessageSubscriptionCoordinatorOptions as i, gatewayOriginScope as j, reduceSessionProjectionRunEvent as k, SessionMessageEnvelope as l, SessionProjectionRun as m, GatewaySessionMessageSubscription as n, getGatewaySessionMessageSubscriptionCoordinator as o, SessionProjectionGatewayRunEvent as p, GatewaySessionMessageSubscriptionCoordinator as r, releaseGatewaySessionMessageSubscription as s, GatewaySessionMessageRequestClient as t, SessionMessageIdentity as u, SessionProjectionSnapshotOptions as v, projectLiveSessionMessage as w, hasSessionProjectionAcceptedFinal as x, SessionProjectionState as y };
180
+ export { gatewayCredentialScope as A, reduceSessionProjection as C, normalizeSessionProjectionRunId as D, SessionMessageIdentity as E, buildDeviceAuthPayload as M, buildDeviceAuthPayloadV3 as N, readSessionMessageIdentity as O, normalizeDeviceMetadataForAuth as P, reconcileSessionProjectionSnapshot as S, SessionMessageEnvelope as T, SessionProjectionState as _, GatewaySessionMessageSubscriptionOptions as a, isLocallyOptimisticSessionMessage as b, resetGatewaySessionMessageSubscriptionCoordinator as c, SessionProjectionGatewayRunEvent as d, SessionProjectionRun as f, SessionProjectionSnapshotOptions as g, SessionProjectionScope as h, GatewaySessionMessageSubscriptionCoordinatorOptions as i, gatewayOriginScope as j, readSessionMessageSequence as k, SessionProjectionEntry as l, SessionProjectionRunTransition as m, GatewaySessionMessageSubscription as n, getGatewaySessionMessageSubscriptionCoordinator as o, SessionProjectionRunStatus as p, GatewaySessionMessageSubscriptionCoordinator as r, releaseGatewaySessionMessageSubscription as s, GatewaySessionMessageRequestClient as t, SessionProjectionEvent as u, createSessionProjection as v, reduceSessionProjectionRunEvent as w, projectLiveSessionMessage as x, hasSessionProjectionAcceptedFinal as y };
@@ -1,4 +1,4 @@
1
- import { clearGatewayConnectTimeout, resolveSafeTimeoutDelayMs, startGatewayConnectTimeout } from "./timeouts.mjs";
1
+ import { DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS, 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
@@ -462,8 +462,7 @@ var GatewayPendingRequests = class {
462
462
  constructor(opts) {
463
463
  this.opts = opts;
464
464
  this.pending = /* @__PURE__ */ new Map();
465
- this.retiredIds = /* @__PURE__ */ new Set();
466
- this.collisionSuffix = 0;
465
+ this.requestSequence = 0;
467
466
  }
468
467
  get hasPending() {
469
468
  return this.pending.size > 0;
@@ -500,7 +499,6 @@ var GatewayPendingRequests = class {
500
499
  const retire = (errorCode) => {
501
500
  if (this.pending.get(id) !== pending) return false;
502
501
  this.pending.delete(id);
503
- this.retiredIds.add(id);
504
502
  cleanup();
505
503
  this.finishTiming(id, pending, false, errorCode);
506
504
  return true;
@@ -564,36 +562,36 @@ var GatewayPendingRequests = class {
564
562
  pending.reject(this.opts.createRequestError?.(frame.error ?? {}) ?? new GatewayProtocolRequestError(frame.error ?? {}));
565
563
  }
566
564
  flush(error) {
567
- for (const [id, pending] of this.pending) {
568
- this.finishTiming(id, pending, false, "CLIENT_CLOSED");
565
+ const retired = [...this.pending];
566
+ this.pending.clear();
567
+ this.requestSequence = 0;
568
+ for (const [id, pending] of retired) {
569
569
  pending.cleanup?.();
570
+ this.finishTiming(id, pending, false, "CLIENT_CLOSED");
570
571
  pending.reject(error);
571
572
  }
572
- this.pending.clear();
573
- this.retiredIds.clear();
574
- this.collisionSuffix = 0;
575
573
  }
576
574
  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;
575
+ this.requestSequence += 1;
576
+ return `${this.requestSequence}:${this.opts.createRequestId()}`;
585
577
  }
586
578
  finishTiming(id, pending, ok, errorCode) {
587
579
  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
- }));
580
+ try {
581
+ const onTiming = this.opts.onTiming;
582
+ if (onTiming === void 0 || onTiming === null) return;
583
+ Reflect.apply(onTiming, this.opts, [{
584
+ id,
585
+ method: pending.method,
586
+ ok,
587
+ durationMs: Math.max(0, endedAtMs - pending.startedAtMs),
588
+ startedAtMs: pending.startedAtMs,
589
+ endedAtMs,
590
+ errorCode
591
+ }]);
592
+ } catch (error) {
593
+ this.opts.onCallbackError?.("request timing", error);
594
+ }
597
595
  }
598
596
  invoke(label, callback) {
599
597
  try {
@@ -733,8 +731,12 @@ var GatewayProtocolClient = class {
733
731
  const normalized = error instanceof Error ? error : new Error(String(error));
734
732
  this.opts.onSocketFactoryError?.(normalized);
735
733
  this.opts.onConnectError?.(normalized);
736
- if (this.opts.rethrowSocketFactoryError?.(normalized)) throw normalized;
734
+ if (this.opts.rethrowSocketFactoryError?.(normalized)) {
735
+ if (this.generation > 0 && !this.stopped && !this.socket && !this.reconnectSignal) this.opts.onReconnectStopped?.(normalized);
736
+ throw normalized;
737
+ }
737
738
  if (this.opts.shouldRetrySocketFactoryError?.(normalized) && !this.stopped && !this.socket && !this.reconnectSignal) this.scheduleReconnect();
739
+ else if (this.generation > 0 && !this.stopped && !this.socket && !this.reconnectSignal) this.opts.onReconnectStopped?.(normalized);
738
740
  return;
739
741
  }
740
742
  this.generation = generation;
@@ -927,7 +929,7 @@ var GatewayProtocolClient = class {
927
929
  const decision = this.opts.resolveClose(context);
928
930
  this.requests.flush(decision.pendingError ?? context.connectFailure?.error ?? /* @__PURE__ */ new Error(`gateway closed (${code}): ${reason}`));
929
931
  this.invoke("close", () => this.opts.onClose?.(context, decision));
930
- if (decision.retry && !this.stopped) this.scheduleReconnect(decision.reconnectDelayMs ?? context.connectFailure?.reconnectDelayMs);
932
+ if (decision.retry && !this.stopped && !this.socket && !this.reconnectSignal) this.scheduleReconnect(decision.reconnectDelayMs ?? context.connectFailure?.reconnectDelayMs);
931
933
  }
932
934
  handleSocketError(socket, generation, error) {
933
935
  if (!this.isActive(socket, generation) || this.connectSent) return;
@@ -942,7 +944,7 @@ var GatewayProtocolClient = class {
942
944
  sleepWithAbort(retry.delayMs, retry.signal).then(() => {
943
945
  if (this.reconnectSignal !== retry.signal) return;
944
946
  this.reconnectSignal = null;
945
- this.connect();
947
+ this.invoke("reconnect", () => this.connect());
946
948
  }, () => {
947
949
  if (this.reconnectSignal === retry.signal) this.reconnectSignal = null;
948
950
  });
@@ -983,6 +985,7 @@ const NON_RECOVERABLE_AUTH_ERRORS = /* @__PURE__ */ new Set([
983
985
  ConnectErrorDetailCodes.AUTH_RATE_LIMITED,
984
986
  ConnectErrorDetailCodes.AUTH_DEVICE_TOKEN_MISMATCH,
985
987
  ConnectErrorDetailCodes.AUTH_SCOPE_MISMATCH,
988
+ ConnectErrorDetailCodes.AUTH_IDENTITY_HEADER_REQUIRED,
986
989
  ConnectErrorDetailCodes.CONTROL_UI_BUILD_MISMATCH,
987
990
  ConnectErrorDetailCodes.PAIRING_REQUIRED,
988
991
  ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED,
@@ -994,15 +997,74 @@ function shouldPauseGatewayReconnect(params) {
994
997
  const pairing = readPairingConnectErrorDetails(params.details);
995
998
  if (code === ConnectErrorDetailCodes.PAIRING_REQUIRED && (pairing?.pauseReconnect === false || pairing?.recommendedNextStep === "wait_then_retry")) return false;
996
999
  if (code === ConnectErrorDetailCodes.AUTH_TOKEN_MISMATCH) return params.tokenMismatchIsTerminal === true && !params.deviceTokenRetryPending;
1000
+ if (code === ConnectErrorDetailCodes.AUTH_IDENTITY_HEADER_REQUIRED) return !params.deviceTokenRetryPending;
997
1001
  return NON_RECOVERABLE_AUTH_ERRORS.has(code) || params.protocolMismatchIsTerminal === true && code === ConnectErrorDetailCodes.PROTOCOL_MISMATCH || params.clientVersionMismatchIsTerminal === true && code === ConnectErrorDetailCodes.CLIENT_VERSION_MISMATCH;
998
1002
  }
999
1003
  //#endregion
1004
+ //#region packages/normalization-core/src/record-coerce.ts
1005
+ /** Type guard for non-array object records at browser-safe boundaries. */
1006
+ function isRecord(value) {
1007
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1008
+ }
1009
+ /** Returns a non-array record or null. */
1010
+ function asNullableRecord(value) {
1011
+ return isRecord(value) ? value : null;
1012
+ }
1013
+ //#endregion
1014
+ //#region packages/gateway-client/src/session-projection-message-identity.ts
1015
+ function readSessionProjectionString(value) {
1016
+ return typeof value === "string" ? value.trim() || null : null;
1017
+ }
1018
+ function readPositiveSafeInteger(value) {
1019
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
1020
+ }
1021
+ /** History and status markers carry transcript order even when they have no chat role. */
1022
+ function readSessionMessageSequence(message, envelope) {
1023
+ return readPositiveSafeInteger(asNullableRecord(asNullableRecord(message)?.["__openclaw"])?.seq) ?? readPositiveSafeInteger(envelope?.messageSeq);
1024
+ }
1025
+ /** Run ownership normalizes a user-turn suffix without changing its persisted send key. */
1026
+ function normalizeSessionProjectionRunId(value) {
1027
+ const runId = readSessionProjectionString(value);
1028
+ return runId?.endsWith(":user") ? runId.slice(0, -5) || null : runId;
1029
+ }
1030
+ /** Persisted row facts win; assistant run ownership comes from its authoritative producer. */
1031
+ function readSessionMessageIdentity(message, envelope) {
1032
+ const record = asNullableRecord(message);
1033
+ const role = readSessionProjectionString(record?.role)?.toLowerCase();
1034
+ if (!record || !role) return null;
1035
+ const metadata = asNullableRecord(record["__openclaw"]);
1036
+ const importedFrom = readSessionProjectionString(metadata?.importedFrom);
1037
+ const cliSessionId = readSessionProjectionString(metadata?.cliSessionId);
1038
+ const externalId = readSessionProjectionString(metadata?.externalId);
1039
+ const idempotencyKey = readSessionProjectionString(metadata?.idempotencyKey) ?? readSessionProjectionString(record.idempotencyKey) ?? readSessionProjectionString(envelope?.idempotencyKey) ?? readSessionProjectionString(envelope?.clientRunId);
1040
+ const persistedRunId = normalizeSessionProjectionRunId(idempotencyKey);
1041
+ const envelopeRunId = normalizeSessionProjectionRunId(envelope?.runId);
1042
+ const metadataRunId = normalizeSessionProjectionRunId(metadata?.runId);
1043
+ const mirroredMessage = readSessionProjectionString(metadata?.mirrorOrigin) !== null;
1044
+ const isCliAssistant = role === "assistant" && readSessionProjectionString(record.api)?.toLowerCase() === "cli";
1045
+ const canonicalPersistedRunId = isCliAssistant && persistedRunId?.startsWith("cli-assistant:") ? readSessionProjectionString(persistedRunId.slice(14)) : persistedRunId;
1046
+ const optimisticRunId = metadata && Object.keys(metadata).every((key) => key === "idempotencyKey") ? canonicalPersistedRunId : null;
1047
+ return {
1048
+ role,
1049
+ id: readSessionProjectionString(metadata?.id) ?? readSessionProjectionString(envelope?.messageId),
1050
+ sequence: readSessionMessageSequence(message, envelope),
1051
+ idempotencyKey,
1052
+ runId: role === "assistant" ? metadataRunId ?? envelopeRunId ?? (isCliAssistant || !mirroredMessage ? canonicalPersistedRunId : null) ?? optimisticRunId : metadataRunId ?? canonicalPersistedRunId ?? envelopeRunId,
1053
+ isImported: Boolean(importedFrom || cliSessionId || externalId),
1054
+ externalSource: importedFrom && cliSessionId && externalId ? JSON.stringify([
1055
+ importedFrom,
1056
+ cliSessionId,
1057
+ externalId
1058
+ ]) : null
1059
+ };
1060
+ }
1061
+ //#endregion
1000
1062
  //#region packages/gateway-client/src/session-projection-run-event.ts
1001
- function readNonemptyString$1(value) {
1063
+ function readNonemptyString(value) {
1002
1064
  return typeof value === "string" ? value.trim() || null : null;
1003
1065
  }
1004
1066
  function reduceSessionProjectionRunEventImpl(projection, event, scope = {}) {
1005
- const runId = readNonemptyString$1(event.runId);
1067
+ const runId = readNonemptyString(event.runId);
1006
1068
  if (!runId || typeof event.state !== "string" || ![
1007
1069
  "delta",
1008
1070
  "final",
@@ -1010,9 +1072,9 @@ function reduceSessionProjectionRunEventImpl(projection, event, scope = {}) {
1010
1072
  "aborted"
1011
1073
  ].includes(event.state)) return null;
1012
1074
  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);
1075
+ const messageStopReason = isRecord(message) ? readNonemptyString(message.stopReason) : null;
1076
+ const stopReason = readNonemptyString(event.stopReason) ?? messageStopReason;
1077
+ const errorKind = readNonemptyString(event.errorKind);
1016
1078
  const base = {
1017
1079
  runId,
1018
1080
  ...message === void 0 ? {} : { message },
@@ -1047,53 +1109,11 @@ const SESSION_PROJECTION_SCOPE_KEYS = [
1047
1109
  "lifecycleRevision",
1048
1110
  "activeLeafEntryId"
1049
1111
  ];
1050
- function readRecord(value) {
1051
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
1052
- }
1053
- function readNonemptyString(value) {
1054
- return typeof value === "string" ? value.trim() || null : null;
1055
- }
1056
- function readPositiveSafeInteger(value) {
1057
- return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
1058
- }
1059
- /** History and status markers carry transcript order even when they have no chat role. */
1060
- function readSessionMessageSequence(message, envelope) {
1061
- return readPositiveSafeInteger(readRecord(readRecord(message)?.["__openclaw"])?.seq) ?? readPositiveSafeInteger(envelope?.messageSeq);
1062
- }
1063
- /** Run ownership normalizes a user-turn suffix without changing its persisted send key. */
1064
- function normalizeSessionProjectionRunId(value) {
1065
- const runId = readNonemptyString(value);
1066
- return runId?.endsWith(":user") ? runId.slice(0, -5) || null : runId;
1067
- }
1068
- /** Persisted transcript facts win over envelope projections and provider-local import IDs. */
1069
- function readSessionMessageIdentity(message, envelope) {
1070
- const record = readRecord(message);
1071
- const role = readNonemptyString(record?.role)?.toLowerCase();
1072
- if (!record || !role) return null;
1073
- const metadata = readRecord(record["__openclaw"]);
1074
- const importedFrom = readNonemptyString(metadata?.importedFrom);
1075
- const cliSessionId = readNonemptyString(metadata?.cliSessionId);
1076
- const externalId = readNonemptyString(metadata?.externalId);
1077
- const idempotencyKey = readNonemptyString(metadata?.idempotencyKey) ?? readNonemptyString(record.idempotencyKey) ?? readNonemptyString(envelope?.idempotencyKey) ?? readNonemptyString(envelope?.clientRunId);
1078
- return {
1079
- role,
1080
- id: readNonemptyString(metadata?.id) ?? readNonemptyString(envelope?.messageId),
1081
- sequence: readSessionMessageSequence(message, envelope),
1082
- idempotencyKey,
1083
- runId: normalizeSessionProjectionRunId(idempotencyKey) ?? normalizeSessionProjectionRunId(envelope?.runId),
1084
- isImported: Boolean(importedFrom || cliSessionId || externalId),
1085
- externalSource: importedFrom && cliSessionId && externalId ? JSON.stringify([
1086
- importedFrom,
1087
- cliSessionId,
1088
- externalId
1089
- ]) : null
1090
- };
1091
- }
1092
1112
  /** Local turns have no durable transcript metadata beyond their own optional send key. */
1093
1113
  function isLocallyOptimisticSessionMessage(message) {
1094
1114
  const identity = readSessionMessageIdentity(message);
1095
1115
  if (!identity || identity.role !== "user" && identity.role !== "assistant") return false;
1096
- const metadata = readRecord(readRecord(message)?.["__openclaw"]);
1116
+ const metadata = asNullableRecord(asNullableRecord(message)?.["__openclaw"]);
1097
1117
  return !metadata || Object.keys(metadata).every((key) => key === "idempotencyKey");
1098
1118
  }
1099
1119
  function createEntry(message, options) {
@@ -1153,10 +1173,12 @@ function entryMatches(left, right, allowSnapshotPromotion = false) {
1153
1173
  if (sameTranscriptIdentity(left.identity, right.identity)) return true;
1154
1174
  const durableEntry = left.identity?.id ? left : right.identity?.id ? right : null;
1155
1175
  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;
1176
+ const durableMetadata = asNullableRecord(asNullableRecord(durableEntry?.message)?.["__openclaw"]);
1177
+ 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 && (readSessionProjectionString(durableMetadata?.mirrorOrigin) === null || durableMetadata?.runTerminal === true)) return true;
1157
1178
  const persisted = left.identity;
1158
1179
  const observed = right.identity;
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;
1180
+ const persistedMetadata = asNullableRecord(asNullableRecord(left.message)?.["__openclaw"]);
1181
+ 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 && (readSessionProjectionString(persistedMetadata?.mirrorOrigin) === null || persistedMetadata?.runTerminal === true))) return true;
1160
1182
  if (left.pending && right.pending) return Boolean(left.identity?.role === right.identity?.role && left.pendingRunId && left.pendingRunId === right.pendingRunId);
1161
1183
  const pending = left.pending ? left : right.pending ? right : null;
1162
1184
  const authoritative = pending === left ? right : pending === right ? left : null;
@@ -1197,6 +1219,7 @@ function projectLiveSessionMessage(state, message, envelope, scope = {}) {
1197
1219
  if (existingIndex < 0) return withEntries(state, insertEntry(state.entries, incoming, state.runs));
1198
1220
  const existing = state.entries[existingIndex];
1199
1221
  if (existing && existing.message === message && existing.live && !existing.pending) return state;
1222
+ if (existing && !existing.pending && existing.identity?.id && !incoming.identity.id) return state;
1200
1223
  if (existing?.pending && incoming.identity.sequence !== null) {
1201
1224
  const sequence = incoming.identity.sequence;
1202
1225
  return withEntries(state, state.entries.some(({ identity }, index) => identity?.sequence != null && (index < existingIndex ? identity.sequence > sequence : identity.sequence < sequence)) ? insertEntry(state.entries.filter((_, index) => index !== existingIndex), incoming, state.runs) : state.entries.toSpliced(existingIndex, 1, incoming));
@@ -1227,13 +1250,13 @@ function reconcileSessionProjectionSnapshot(state, messages, scope = {}, options
1227
1250
  }
1228
1251
  function hasDisplayableSessionMessage(message) {
1229
1252
  if (typeof message === "string") return message.trim().length > 0;
1230
- const record = readRecord(message);
1253
+ const record = asNullableRecord(message);
1231
1254
  if (!record) return false;
1232
1255
  const displayableBlocks = Array.isArray(record.content) && record.content.some((block) => {
1233
- const entry = readRecord(block);
1234
- return entry ? entry.type !== "text" || readNonemptyString(entry.text) !== null : typeof block === "string" && block.trim().length > 0;
1256
+ const entry = asNullableRecord(block);
1257
+ return entry ? entry.type !== "text" || readSessionProjectionString(entry.text) !== null : typeof block === "string" && block.trim().length > 0;
1235
1258
  });
1236
- const media = readRecord(record["__openclaw"])?.media;
1259
+ const media = asNullableRecord(record["__openclaw"])?.media;
1237
1260
  return Boolean(typeof record.content === "string" && record.content.trim() || displayableBlocks || Array.isArray(media) && media.length > 0);
1238
1261
  }
1239
1262
  function readSessionProjectionFinalMessageIdentity(message) {
@@ -1242,8 +1265,8 @@ function readSessionProjectionFinalMessageIdentity(message) {
1242
1265
  if (identity?.externalSource) return `import:${identity.role}:${identity.externalSource}`;
1243
1266
  if (identity?.id && !identity.isImported) return `id:${identity.role}:${identity.id}`;
1244
1267
  if (identity?.sequence !== null && identity?.sequence !== void 0) return `seq:${identity.role}:${identity.sequence}`;
1245
- const record = readRecord(message);
1246
- const metadata = readRecord(record?.["__openclaw"]);
1268
+ const record = asNullableRecord(message);
1269
+ const metadata = asNullableRecord(record?.["__openclaw"]);
1247
1270
  try {
1248
1271
  return `content:${JSON.stringify([
1249
1272
  identity?.role ?? "assistant",
@@ -1274,7 +1297,7 @@ function retainSessionProjectionRuns(runs) {
1274
1297
  return Object.fromEntries([...active, ...retainedTerminal]);
1275
1298
  }
1276
1299
  function updateRun(state, incoming) {
1277
- const incomingErrorMessage = readNonemptyString(incoming.errorMessage);
1300
+ const incomingErrorMessage = readSessionProjectionString(incoming.errorMessage);
1278
1301
  const normalizedIncoming = { ...incoming };
1279
1302
  if (incomingErrorMessage) normalizedIncoming.errorMessage = incomingErrorMessage;
1280
1303
  else delete normalizedIncoming.errorMessage;
@@ -1285,7 +1308,7 @@ function updateRun(state, incoming) {
1285
1308
  const canRecoverFinal = !hasDisplayableSessionMessage(current.message) || (current.acceptedFinalMessageIdentities?.length ?? 0) > 0;
1286
1309
  const acceptFinal = incomingIsFinal && (current.status === incoming.status || canRecoverFinal) && incomingFinalIdentity !== null && !hasSessionProjectionAcceptedFinal(current, incoming.message);
1287
1310
  const recoverMessage = acceptFinal && !hasDisplayableSessionMessage(current.message);
1288
- const recoverError = readNonemptyString(current.errorMessage) === null && incomingErrorMessage !== null;
1311
+ const recoverError = readSessionProjectionString(current.errorMessage) === null && incomingErrorMessage !== null;
1289
1312
  if (!acceptFinal && !recoverError) return state;
1290
1313
  const firstFinalIdentity = readSessionProjectionFinalMessageIdentity(current.message);
1291
1314
  const previousFinalIdentities = current.acceptedFinalMessageIdentities ?? (firstFinalIdentity ? [firstFinalIdentity] : []);
@@ -1433,7 +1456,7 @@ var GatewaySessionMessageSubscriptionCoordinator = class {
1433
1456
  if (this.#retired) throw new Error("Session message subscription belongs to a replaced Gateway connection");
1434
1457
  const existing = [...this.#entries].find((candidate) => candidate.agentId === agentId && (this.#areKeysEquivalent(candidate.key, normalizedKey) || [...candidate.requestedKeys].some((requestedKey) => this.#areKeysEquivalent(requestedKey, normalizedKey))));
1435
1458
  if (!existing) {
1436
- const provisional = [...this.#entries].find((candidate) => candidate.agentId === agentId && !candidate.canonicalSettled);
1459
+ const provisional = [...this.#entries].find((candidate) => candidate.agentId === agentId && !candidate.canonicalSettled && this.#couldShareCanonicalIdentity(candidate.key, normalizedKey));
1437
1460
  if (provisional) {
1438
1461
  await (provisional.plainFallback ?? provisional.ready).catch(() => void 0);
1439
1462
  continue;
@@ -1489,7 +1512,7 @@ var GatewaySessionMessageSubscriptionCoordinator = class {
1489
1512
  entry.release = tracked;
1490
1513
  return tracked;
1491
1514
  }
1492
- const tracked = this.#client.request("sessions.messages.unsubscribe", sessionSubscriptionParams(entry.key, entry.agentId)).then(() => {
1515
+ const tracked = this.#client.request("sessions.messages.unsubscribe", sessionSubscriptionParams(entry.key, entry.agentId), { timeoutMs: DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS }).then(() => {
1493
1516
  this.#finishRelease(subscription, owner, true);
1494
1517
  }).finally(() => {
1495
1518
  if (entry.release === tracked) entry.release = null;
@@ -1564,9 +1587,18 @@ var GatewaySessionMessageSubscriptionCoordinator = class {
1564
1587
  return upgrade;
1565
1588
  }
1566
1589
  async #requestSubscribe(entry, includeApprovals) {
1567
- const result = await this.#client.request("sessions.messages.subscribe", {
1568
- ...sessionSubscriptionParams(entry.key, entry.agentId),
1569
- ...includeApprovals ? { includeApprovals: true } : {}
1590
+ const params = sessionSubscriptionParams(entry.key, entry.agentId);
1591
+ const result = await this.#client.request("sessions.messages.subscribe", includeApprovals ? {
1592
+ ...params,
1593
+ includeApprovals: true
1594
+ } : params, { timeoutMs: DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS }).catch(async (error) => {
1595
+ if (!(error instanceof GatewayProtocolRequestTimeoutError) || !error.requestSent || this.#retired) throw error;
1596
+ try {
1597
+ await this.#client.request(entry.handles.size > 0 ? "sessions.messages.subscribe" : "sessions.messages.unsubscribe", params, { timeoutMs: DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS });
1598
+ } catch (recoveryError) {
1599
+ if (!this.#retired) throw new AggregateError([error, recoveryError], "session message subscription recovery failed", { cause: recoveryError });
1600
+ }
1601
+ throw error;
1570
1602
  });
1571
1603
  const response = result && typeof result === "object" ? result : null;
1572
1604
  const responseKey = response && "key" in response ? response.key : void 0;
@@ -1584,6 +1616,11 @@ var GatewaySessionMessageSubscriptionCoordinator = class {
1584
1616
  #areKeysEquivalent(left, right) {
1585
1617
  return left === right || this.#keysEquivalent?.(left, right) === true;
1586
1618
  }
1619
+ #couldShareCanonicalIdentity(left, right) {
1620
+ const leftBody = left.replace(/^agent:[^:]+:/i, "").toLowerCase();
1621
+ const rightBody = right.replace(/^agent:[^:]+:/i, "").toLowerCase();
1622
+ return leftBody === rightBody || leftBody === "main" || rightBody === "main" || leftBody === "global" || rightBody === "global";
1623
+ }
1587
1624
  };
1588
1625
  const sessionMessageSubscriptionOwners = /* @__PURE__ */ new WeakMap();
1589
1626
  const sessionMessageSubscriptionCoordinators = /* @__PURE__ */ new WeakMap();
@@ -1602,4 +1639,4 @@ function releaseGatewaySessionMessageSubscription(subscription) {
1602
1639
  return sessionMessageSubscriptionOwners.get(subscription)?.coordinator.release(subscription) ?? Promise.resolve();
1603
1640
  }
1604
1641
  //#endregion
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 };
1642
+ export { buildGatewayConnectAuth as C, buildDeviceAuthPayload as D, shouldRetryGatewayWithDeviceToken as E, buildDeviceAuthPayloadV3 as O, GatewayBrowserDeviceAuthLifecycle as S, selectGatewayConnectAuth as T, GatewayProtocolClient as _, createSessionProjection as a, gatewayCredentialScope as b, projectLiveSessionMessage as c, reduceSessionProjectionRunEvent as d, normalizeSessionProjectionRunId as f, shouldPauseGatewayReconnect as g, isRecord as h, resetGatewaySessionMessageSubscriptionCoordinator as i, normalizeDeviceMetadataForAuth as k, reconcileSessionProjectionSnapshot as l, readSessionMessageSequence as m, getGatewaySessionMessageSubscriptionCoordinator as n, hasSessionProjectionAcceptedFinal as o, readSessionMessageIdentity as p, releaseGatewaySessionMessageSubscription as r, isLocallyOptimisticSessionMessage as s, GatewaySessionMessageSubscriptionCoordinator as t, reduceSessionProjection as u, GatewayProtocolRequestError as v, resolveGatewayConnectScopes as w, gatewayOriginScope as x, GatewayProtocolRequestTimeoutError as y };
@@ -1,5 +1,4 @@
1
1
  import { RawData } from "ws";
2
-
3
2
  //#region packages/gateway-client/src/websocket-data.d.ts
4
3
  declare function rawDataToString(data: RawData, encoding?: BufferEncoding): string;
5
4
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/gateway-client",
3
- "version": "2026.8.1-beta.2",
3
+ "version": "2026.9.1-beta.1",
4
4
  "description": "Reference WebSocket client for the OpenClaw Gateway protocol",
5
5
  "keywords": [
6
6
  "client",
@@ -62,9 +62,12 @@
62
62
  }
63
63
  },
64
64
  "dependencies": {
65
- "ipaddr.js": "2.4.0",
66
- "ws": "8.21.1",
67
- "@openclaw/gateway-protocol": "2026.8.1-beta.2"
65
+ "ipaddr.js": "2.5.0",
66
+ "ws": "8.21.3",
67
+ "@openclaw/gateway-protocol": "2026.9.1-beta.1"
68
+ },
69
+ "devDependencies": {
70
+ "@openclaw/normalization-core": "0.0.0-private"
68
71
  },
69
72
  "engines": {
70
73
  "node": ">=22.19.0"