@openclaw/gateway-client 2026.8.1-beta.2 → 2026.8.1-beta.3

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.
@@ -69,7 +69,8 @@ declare class GatewayClientRequestTimeoutError extends GatewayProtocolRequestTim
69
69
  declare function isGatewayConnectAssemblyError(value: unknown): value is Error;
70
70
  type GatewayClientOptions = {
71
71
  url?: string;
72
- origin?: string;
72
+ origin?: string; /** Already-resolved edge-proxy auth headers (identity-aware proxy in front of the Gateway). */
73
+ edgeAuthHeaders?: Readonly<Record<string, string>>;
73
74
  connectChallengeTimeoutMs?: number;
74
75
  /**
75
76
  * Server-side pre-auth handshake budget. Config-derived local clients use
@@ -98,7 +99,7 @@ type GatewayClientOptions = {
98
99
  scopes?: string[];
99
100
  caps?: string[];
100
101
  commands?: string[];
101
- computerUse?: ConnectParams["computerUse"];
102
+ computerUse?: ConnectParams["computerUse"]; /** @deprecated Compatibility for the shipped v1 node-host connect envelope. */
102
103
  workerRuns?: ConnectParams["workerRuns"];
103
104
  permissions?: Record<string, boolean>;
104
105
  pathEnv?: string;
@@ -111,7 +112,8 @@ type GatewayClientOptions = {
111
112
  onEvent?: (evt: EventFrame) => void;
112
113
  onHelloOk?: (hello: HelloOk) => void;
113
114
  onConnectError?: (err: Error) => void;
114
- onReconnectPaused?: (info: GatewayReconnectPausedInfo) => void;
115
+ onReconnectPaused?: (info: GatewayReconnectPausedInfo) => void; /** Report retryable startup closes for clients that present connection progress. */
116
+ notifyOnStartupRetry?: boolean;
115
117
  onClose?: (code: number, reason: string, info?: GatewayClientCloseInfo) => void;
116
118
  onGap?: (info: {
117
119
  expected: number;
@@ -149,7 +151,7 @@ declare class GatewayClient {
149
151
  updateNodeManifest(manifest: {
150
152
  caps: string[];
151
153
  commands: string[];
152
- computerUse?: ConnectParams["computerUse"];
154
+ computerUse?: ConnectParams["computerUse"]; /** @deprecated Compatibility for the shipped v1 node-host connect envelope. */
153
155
  workerRuns?: ConnectParams["workerRuns"];
154
156
  }): void;
155
157
  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-JllUDKLg.mjs";
2
2
  export { EventLoopReadyWaiter, GatewayClientStartReadinessOptions, GatewayClientStartable, startGatewayClientWhenEventLoopReady, startGatewayClientWithReadinessWait };
@@ -1,3 +1,5 @@
1
+ import { n as GatewayProtocolRequestOptions } from "./protocol-request-D4mVhLxE.mjs";
2
+
1
3
  //#region packages/gateway-client/src/device-auth.d.ts
2
4
  declare function normalizeDeviceMetadataForAuth(value?: string | null): string;
3
5
  type DeviceAuthPayloadParams = {
@@ -23,8 +25,7 @@ declare function gatewayOriginScope(gatewayUrl: string): string;
23
25
  /** Normalizes the gateway URL scope used for browser credential records. */
24
26
  declare function gatewayCredentialScope(gatewayUrl: string): string;
25
27
  //#endregion
26
- //#region packages/gateway-client/src/session-projection.d.ts
27
- /** Browser-safe identity and replay rules shared by Gateway conversation clients. */
28
+ //#region packages/gateway-client/src/session-projection-message-identity.d.ts
28
29
  type SessionMessageEnvelope = {
29
30
  messageId?: unknown;
30
31
  messageSeq?: unknown;
@@ -41,6 +42,14 @@ type SessionMessageIdentity = {
41
42
  isImported: boolean;
42
43
  externalSource: string | null;
43
44
  };
45
+ /** History and status markers carry transcript order even when they have no chat role. */
46
+ declare function readSessionMessageSequence(message: unknown, envelope?: SessionMessageEnvelope): number | null;
47
+ /** Run ownership normalizes a user-turn suffix without changing its persisted send key. */
48
+ declare function normalizeSessionProjectionRunId(value: unknown): string | null;
49
+ /** Persisted row facts win; assistant run ownership comes from its authoritative producer. */
50
+ declare function readSessionMessageIdentity(message: unknown, envelope?: SessionMessageEnvelope): SessionMessageIdentity | null;
51
+ //#endregion
52
+ //#region packages/gateway-client/src/session-projection.d.ts
44
53
  type SessionProjectionScope = {
45
54
  sessionKey?: string;
46
55
  sessionId?: string;
@@ -122,12 +131,6 @@ type SessionProjectionEvent = ScopedSessionProjectionEvent & ({
122
131
  } | {
123
132
  type: "reconnected";
124
133
  });
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
134
  /** Local turns have no durable transcript metadata beyond their own optional send key. */
132
135
  declare function isLocallyOptimisticSessionMessage(message: unknown): boolean;
133
136
  declare function createSessionProjection(scope?: SessionProjectionScope, messages?: readonly unknown[]): SessionProjectionState;
@@ -143,7 +146,7 @@ declare function reduceSessionProjectionRunEvent(projection: SessionProjectionSt
143
146
  //#endregion
144
147
  //#region packages/gateway-client/src/session-subscriptions.d.ts
145
148
  type GatewaySessionMessageRequestClient = {
146
- request<T = unknown>(method: string, params: Record<string, unknown>): Promise<T>;
149
+ request<T = unknown>(method: string, params: Record<string, unknown>, options?: GatewayProtocolRequestOptions): Promise<T>;
147
150
  };
148
151
  type GatewaySessionMessageSubscription = {
149
152
  key: string;
@@ -175,4 +178,4 @@ declare function getGatewaySessionMessageSubscriptionCoordinator(client: Gateway
175
178
  declare function resetGatewaySessionMessageSubscriptionCoordinator(client: GatewaySessionMessageRequestClient): void;
176
179
  declare function releaseGatewaySessionMessageSubscription(subscription: GatewaySessionMessageSubscription): Promise<void>;
177
180
  //#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 };
181
+ 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;
@@ -570,30 +568,29 @@ var GatewayPendingRequests = class {
570
568
  pending.reject(error);
571
569
  }
572
570
  this.pending.clear();
573
- this.retiredIds.clear();
574
- this.collisionSuffix = 0;
571
+ this.requestSequence = 0;
575
572
  }
576
573
  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;
574
+ this.requestSequence += 1;
575
+ return `${this.requestSequence}:${this.opts.createRequestId()}`;
585
576
  }
586
577
  finishTiming(id, pending, ok, errorCode) {
587
578
  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
- }));
579
+ try {
580
+ const onTiming = this.opts.onTiming;
581
+ if (onTiming === void 0 || onTiming === null) return;
582
+ Reflect.apply(onTiming, this.opts, [{
583
+ id,
584
+ method: pending.method,
585
+ ok,
586
+ durationMs: Math.max(0, endedAtMs - pending.startedAtMs),
587
+ startedAtMs: pending.startedAtMs,
588
+ endedAtMs,
589
+ errorCode
590
+ }]);
591
+ } catch (error) {
592
+ this.opts.onCallbackError?.("request timing", error);
593
+ }
597
594
  }
598
595
  invoke(label, callback) {
599
596
  try {
@@ -983,6 +980,7 @@ const NON_RECOVERABLE_AUTH_ERRORS = /* @__PURE__ */ new Set([
983
980
  ConnectErrorDetailCodes.AUTH_RATE_LIMITED,
984
981
  ConnectErrorDetailCodes.AUTH_DEVICE_TOKEN_MISMATCH,
985
982
  ConnectErrorDetailCodes.AUTH_SCOPE_MISMATCH,
983
+ ConnectErrorDetailCodes.AUTH_IDENTITY_HEADER_REQUIRED,
986
984
  ConnectErrorDetailCodes.CONTROL_UI_BUILD_MISMATCH,
987
985
  ConnectErrorDetailCodes.PAIRING_REQUIRED,
988
986
  ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED,
@@ -994,15 +992,74 @@ function shouldPauseGatewayReconnect(params) {
994
992
  const pairing = readPairingConnectErrorDetails(params.details);
995
993
  if (code === ConnectErrorDetailCodes.PAIRING_REQUIRED && (pairing?.pauseReconnect === false || pairing?.recommendedNextStep === "wait_then_retry")) return false;
996
994
  if (code === ConnectErrorDetailCodes.AUTH_TOKEN_MISMATCH) return params.tokenMismatchIsTerminal === true && !params.deviceTokenRetryPending;
995
+ if (code === ConnectErrorDetailCodes.AUTH_IDENTITY_HEADER_REQUIRED) return !params.deviceTokenRetryPending;
997
996
  return NON_RECOVERABLE_AUTH_ERRORS.has(code) || params.protocolMismatchIsTerminal === true && code === ConnectErrorDetailCodes.PROTOCOL_MISMATCH || params.clientVersionMismatchIsTerminal === true && code === ConnectErrorDetailCodes.CLIENT_VERSION_MISMATCH;
998
997
  }
999
998
  //#endregion
999
+ //#region packages/normalization-core/src/record-coerce.ts
1000
+ /** Type guard for non-array object records at browser-safe boundaries. */
1001
+ function isRecord(value) {
1002
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1003
+ }
1004
+ /** Returns a non-array record or null. */
1005
+ function asNullableRecord(value) {
1006
+ return isRecord(value) ? value : null;
1007
+ }
1008
+ //#endregion
1009
+ //#region packages/gateway-client/src/session-projection-message-identity.ts
1010
+ function readSessionProjectionString(value) {
1011
+ return typeof value === "string" ? value.trim() || null : null;
1012
+ }
1013
+ function readPositiveSafeInteger(value) {
1014
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
1015
+ }
1016
+ /** History and status markers carry transcript order even when they have no chat role. */
1017
+ function readSessionMessageSequence(message, envelope) {
1018
+ return readPositiveSafeInteger(asNullableRecord(asNullableRecord(message)?.["__openclaw"])?.seq) ?? readPositiveSafeInteger(envelope?.messageSeq);
1019
+ }
1020
+ /** Run ownership normalizes a user-turn suffix without changing its persisted send key. */
1021
+ function normalizeSessionProjectionRunId(value) {
1022
+ const runId = readSessionProjectionString(value);
1023
+ return runId?.endsWith(":user") ? runId.slice(0, -5) || null : runId;
1024
+ }
1025
+ /** Persisted row facts win; assistant run ownership comes from its authoritative producer. */
1026
+ function readSessionMessageIdentity(message, envelope) {
1027
+ const record = asNullableRecord(message);
1028
+ const role = readSessionProjectionString(record?.role)?.toLowerCase();
1029
+ if (!record || !role) return null;
1030
+ const metadata = asNullableRecord(record["__openclaw"]);
1031
+ const importedFrom = readSessionProjectionString(metadata?.importedFrom);
1032
+ const cliSessionId = readSessionProjectionString(metadata?.cliSessionId);
1033
+ const externalId = readSessionProjectionString(metadata?.externalId);
1034
+ const idempotencyKey = readSessionProjectionString(metadata?.idempotencyKey) ?? readSessionProjectionString(record.idempotencyKey) ?? readSessionProjectionString(envelope?.idempotencyKey) ?? readSessionProjectionString(envelope?.clientRunId);
1035
+ const persistedRunId = normalizeSessionProjectionRunId(idempotencyKey);
1036
+ const envelopeRunId = normalizeSessionProjectionRunId(envelope?.runId);
1037
+ const metadataRunId = normalizeSessionProjectionRunId(metadata?.runId);
1038
+ const mirroredMessage = readSessionProjectionString(metadata?.mirrorOrigin) !== null;
1039
+ const isCliAssistant = role === "assistant" && readSessionProjectionString(record.api)?.toLowerCase() === "cli";
1040
+ const canonicalPersistedRunId = isCliAssistant && persistedRunId?.startsWith("cli-assistant:") ? readSessionProjectionString(persistedRunId.slice(14)) : persistedRunId;
1041
+ const optimisticRunId = metadata && Object.keys(metadata).every((key) => key === "idempotencyKey") ? canonicalPersistedRunId : null;
1042
+ return {
1043
+ role,
1044
+ id: readSessionProjectionString(metadata?.id) ?? readSessionProjectionString(envelope?.messageId),
1045
+ sequence: readSessionMessageSequence(message, envelope),
1046
+ idempotencyKey,
1047
+ runId: role === "assistant" ? metadataRunId ?? envelopeRunId ?? (isCliAssistant || !mirroredMessage ? canonicalPersistedRunId : null) ?? optimisticRunId : metadataRunId ?? canonicalPersistedRunId ?? envelopeRunId,
1048
+ isImported: Boolean(importedFrom || cliSessionId || externalId),
1049
+ externalSource: importedFrom && cliSessionId && externalId ? JSON.stringify([
1050
+ importedFrom,
1051
+ cliSessionId,
1052
+ externalId
1053
+ ]) : null
1054
+ };
1055
+ }
1056
+ //#endregion
1000
1057
  //#region packages/gateway-client/src/session-projection-run-event.ts
1001
- function readNonemptyString$1(value) {
1058
+ function readNonemptyString(value) {
1002
1059
  return typeof value === "string" ? value.trim() || null : null;
1003
1060
  }
1004
1061
  function reduceSessionProjectionRunEventImpl(projection, event, scope = {}) {
1005
- const runId = readNonemptyString$1(event.runId);
1062
+ const runId = readNonemptyString(event.runId);
1006
1063
  if (!runId || typeof event.state !== "string" || ![
1007
1064
  "delta",
1008
1065
  "final",
@@ -1010,9 +1067,9 @@ function reduceSessionProjectionRunEventImpl(projection, event, scope = {}) {
1010
1067
  "aborted"
1011
1068
  ].includes(event.state)) return null;
1012
1069
  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);
1070
+ const messageStopReason = isRecord(message) ? readNonemptyString(message.stopReason) : null;
1071
+ const stopReason = readNonemptyString(event.stopReason) ?? messageStopReason;
1072
+ const errorKind = readNonemptyString(event.errorKind);
1016
1073
  const base = {
1017
1074
  runId,
1018
1075
  ...message === void 0 ? {} : { message },
@@ -1047,53 +1104,11 @@ const SESSION_PROJECTION_SCOPE_KEYS = [
1047
1104
  "lifecycleRevision",
1048
1105
  "activeLeafEntryId"
1049
1106
  ];
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
1107
  /** Local turns have no durable transcript metadata beyond their own optional send key. */
1093
1108
  function isLocallyOptimisticSessionMessage(message) {
1094
1109
  const identity = readSessionMessageIdentity(message);
1095
1110
  if (!identity || identity.role !== "user" && identity.role !== "assistant") return false;
1096
- const metadata = readRecord(readRecord(message)?.["__openclaw"]);
1111
+ const metadata = asNullableRecord(asNullableRecord(message)?.["__openclaw"]);
1097
1112
  return !metadata || Object.keys(metadata).every((key) => key === "idempotencyKey");
1098
1113
  }
1099
1114
  function createEntry(message, options) {
@@ -1153,10 +1168,12 @@ function entryMatches(left, right, allowSnapshotPromotion = false) {
1153
1168
  if (sameTranscriptIdentity(left.identity, right.identity)) return true;
1154
1169
  const durableEntry = left.identity?.id ? left : right.identity?.id ? right : null;
1155
1170
  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;
1171
+ const durableMetadata = asNullableRecord(asNullableRecord(durableEntry?.message)?.["__openclaw"]);
1172
+ 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
1173
  const persisted = left.identity;
1158
1174
  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;
1175
+ const persistedMetadata = asNullableRecord(asNullableRecord(left.message)?.["__openclaw"]);
1176
+ 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
1177
  if (left.pending && right.pending) return Boolean(left.identity?.role === right.identity?.role && left.pendingRunId && left.pendingRunId === right.pendingRunId);
1161
1178
  const pending = left.pending ? left : right.pending ? right : null;
1162
1179
  const authoritative = pending === left ? right : pending === right ? left : null;
@@ -1197,6 +1214,7 @@ function projectLiveSessionMessage(state, message, envelope, scope = {}) {
1197
1214
  if (existingIndex < 0) return withEntries(state, insertEntry(state.entries, incoming, state.runs));
1198
1215
  const existing = state.entries[existingIndex];
1199
1216
  if (existing && existing.message === message && existing.live && !existing.pending) return state;
1217
+ if (existing && !existing.pending && existing.identity?.id && !incoming.identity.id) return state;
1200
1218
  if (existing?.pending && incoming.identity.sequence !== null) {
1201
1219
  const sequence = incoming.identity.sequence;
1202
1220
  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 +1245,13 @@ function reconcileSessionProjectionSnapshot(state, messages, scope = {}, options
1227
1245
  }
1228
1246
  function hasDisplayableSessionMessage(message) {
1229
1247
  if (typeof message === "string") return message.trim().length > 0;
1230
- const record = readRecord(message);
1248
+ const record = asNullableRecord(message);
1231
1249
  if (!record) return false;
1232
1250
  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;
1251
+ const entry = asNullableRecord(block);
1252
+ return entry ? entry.type !== "text" || readSessionProjectionString(entry.text) !== null : typeof block === "string" && block.trim().length > 0;
1235
1253
  });
1236
- const media = readRecord(record["__openclaw"])?.media;
1254
+ const media = asNullableRecord(record["__openclaw"])?.media;
1237
1255
  return Boolean(typeof record.content === "string" && record.content.trim() || displayableBlocks || Array.isArray(media) && media.length > 0);
1238
1256
  }
1239
1257
  function readSessionProjectionFinalMessageIdentity(message) {
@@ -1242,8 +1260,8 @@ function readSessionProjectionFinalMessageIdentity(message) {
1242
1260
  if (identity?.externalSource) return `import:${identity.role}:${identity.externalSource}`;
1243
1261
  if (identity?.id && !identity.isImported) return `id:${identity.role}:${identity.id}`;
1244
1262
  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"]);
1263
+ const record = asNullableRecord(message);
1264
+ const metadata = asNullableRecord(record?.["__openclaw"]);
1247
1265
  try {
1248
1266
  return `content:${JSON.stringify([
1249
1267
  identity?.role ?? "assistant",
@@ -1274,7 +1292,7 @@ function retainSessionProjectionRuns(runs) {
1274
1292
  return Object.fromEntries([...active, ...retainedTerminal]);
1275
1293
  }
1276
1294
  function updateRun(state, incoming) {
1277
- const incomingErrorMessage = readNonemptyString(incoming.errorMessage);
1295
+ const incomingErrorMessage = readSessionProjectionString(incoming.errorMessage);
1278
1296
  const normalizedIncoming = { ...incoming };
1279
1297
  if (incomingErrorMessage) normalizedIncoming.errorMessage = incomingErrorMessage;
1280
1298
  else delete normalizedIncoming.errorMessage;
@@ -1285,7 +1303,7 @@ function updateRun(state, incoming) {
1285
1303
  const canRecoverFinal = !hasDisplayableSessionMessage(current.message) || (current.acceptedFinalMessageIdentities?.length ?? 0) > 0;
1286
1304
  const acceptFinal = incomingIsFinal && (current.status === incoming.status || canRecoverFinal) && incomingFinalIdentity !== null && !hasSessionProjectionAcceptedFinal(current, incoming.message);
1287
1305
  const recoverMessage = acceptFinal && !hasDisplayableSessionMessage(current.message);
1288
- const recoverError = readNonemptyString(current.errorMessage) === null && incomingErrorMessage !== null;
1306
+ const recoverError = readSessionProjectionString(current.errorMessage) === null && incomingErrorMessage !== null;
1289
1307
  if (!acceptFinal && !recoverError) return state;
1290
1308
  const firstFinalIdentity = readSessionProjectionFinalMessageIdentity(current.message);
1291
1309
  const previousFinalIdentities = current.acceptedFinalMessageIdentities ?? (firstFinalIdentity ? [firstFinalIdentity] : []);
@@ -1489,7 +1507,7 @@ var GatewaySessionMessageSubscriptionCoordinator = class {
1489
1507
  entry.release = tracked;
1490
1508
  return tracked;
1491
1509
  }
1492
- const tracked = this.#client.request("sessions.messages.unsubscribe", sessionSubscriptionParams(entry.key, entry.agentId)).then(() => {
1510
+ const tracked = this.#client.request("sessions.messages.unsubscribe", sessionSubscriptionParams(entry.key, entry.agentId), { timeoutMs: DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS }).then(() => {
1493
1511
  this.#finishRelease(subscription, owner, true);
1494
1512
  }).finally(() => {
1495
1513
  if (entry.release === tracked) entry.release = null;
@@ -1564,9 +1582,18 @@ var GatewaySessionMessageSubscriptionCoordinator = class {
1564
1582
  return upgrade;
1565
1583
  }
1566
1584
  async #requestSubscribe(entry, includeApprovals) {
1567
- const result = await this.#client.request("sessions.messages.subscribe", {
1568
- ...sessionSubscriptionParams(entry.key, entry.agentId),
1569
- ...includeApprovals ? { includeApprovals: true } : {}
1585
+ const params = sessionSubscriptionParams(entry.key, entry.agentId);
1586
+ const result = await this.#client.request("sessions.messages.subscribe", includeApprovals ? {
1587
+ ...params,
1588
+ includeApprovals: true
1589
+ } : params, { timeoutMs: DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS }).catch(async (error) => {
1590
+ if (!(error instanceof GatewayProtocolRequestTimeoutError) || !error.requestSent || this.#retired) throw error;
1591
+ try {
1592
+ await this.#client.request(entry.handles.size > 0 ? "sessions.messages.subscribe" : "sessions.messages.unsubscribe", params, { timeoutMs: DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS });
1593
+ } catch (recoveryError) {
1594
+ if (!this.#retired) throw new AggregateError([error, recoveryError], "session message subscription recovery failed", { cause: recoveryError });
1595
+ }
1596
+ throw error;
1570
1597
  });
1571
1598
  const response = result && typeof result === "object" ? result : null;
1572
1599
  const responseKey = response && "key" in response ? response.key : void 0;
@@ -1602,4 +1629,4 @@ function releaseGatewaySessionMessageSubscription(subscription) {
1602
1629
  return sessionMessageSubscriptionOwners.get(subscription)?.coordinator.release(subscription) ?? Promise.resolve();
1603
1630
  }
1604
1631
  //#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 };
1632
+ 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, projectLiveSessionMessage as c, reduceSessionProjectionRunEvent as d, normalizeSessionProjectionRunId as f, GatewayProtocolClient as g, shouldPauseGatewayReconnect as h, resetGatewaySessionMessageSubscriptionCoordinator as i, 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, GatewayProtocolRequestTimeoutError as v, selectGatewayConnectAuth as w, GatewayBrowserDeviceAuthLifecycle as x, gatewayCredentialScope as y };
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.8.1-beta.3",
4
4
  "description": "Reference WebSocket client for the OpenClaw Gateway protocol",
5
5
  "keywords": [
6
6
  "client",
@@ -64,7 +64,10 @@
64
64
  "dependencies": {
65
65
  "ipaddr.js": "2.4.0",
66
66
  "ws": "8.21.1",
67
- "@openclaw/gateway-protocol": "2026.8.1-beta.2"
67
+ "@openclaw/gateway-protocol": "2026.8.1-beta.3"
68
+ },
69
+ "devDependencies": {
70
+ "@openclaw/normalization-core": "0.0.0-private"
68
71
  },
69
72
  "engines": {
70
73
  "node": ">=22.19.0"