@openclaw/gateway-client 2026.8.1-beta.2 → 2026.8.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-CcYZeK09.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;
@@ -14,6 +13,7 @@ declare class GatewayProtocolRequestError extends Error {
14
13
  readonly details?: unknown;
15
14
  readonly retryable: boolean;
16
15
  readonly retryAfterMs?: number;
16
+ readonly responsePayload?: unknown;
17
17
  constructor(error: Partial<ErrorShape>);
18
18
  }
19
19
  /** A local transport deadline, distinct from a Gateway's authoritative rejection. */
@@ -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-CcYZeK09.mjs";
2
+ import "./protocol-client-BfrZgB1l.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-bH1mOPOT.mjs";
2
2
  export { EventLoopReadyWaiter, GatewayClientStartReadinessOptions, GatewayClientStartable, startGatewayClientWhenEventLoopReady, startGatewayClientWithReadinessWait };
@@ -1,13 +1,12 @@
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-CcYZeK09.mjs";
4
3
  //#region packages/gateway-client/src/scope-upgrade.d.ts
5
- type ScopeUpgradeBinding = {
4
+ export type ScopeUpgradeBinding = {
6
5
  clientId: string;
7
6
  deviceId: string;
8
7
  role: string;
9
8
  };
10
- type ScopeUpgradeOutcome = {
9
+ export type ScopeUpgradeOutcome = {
11
10
  status: "approved";
12
11
  requestId: string;
13
12
  scopes: string[];
@@ -15,14 +14,14 @@ type ScopeUpgradeOutcome = {
15
14
  status: "rejected" | "expired";
16
15
  requestId: string;
17
16
  };
18
- type ScopeUpgradeOptions = {
17
+ export type ScopeUpgradeOptions = {
19
18
  binding: ScopeUpgradeBinding;
20
19
  scopes: readonly string[];
21
20
  onPending?: (requestId: string) => void;
22
21
  };
23
22
  type UpgradeRequester = (method: string, params?: unknown, options?: GatewayProtocolRequestOptions) => Promise<unknown>;
24
23
  /** Runs one browser device scope upgrade and owns rotated-token persistence. */
25
- declare class GatewayScopeUpgrade {
24
+ export declare class GatewayScopeUpgrade {
26
25
  private readonly deps;
27
26
  private active?;
28
27
  constructor(deps: {
@@ -34,5 +33,4 @@ declare class GatewayScopeUpgrade {
34
33
  cancelScopeUpgrade(): void;
35
34
  private runUpgrade;
36
35
  }
37
- //#endregion
38
- export { GatewayScopeUpgrade, ScopeUpgradeBinding, ScopeUpgradeOptions, ScopeUpgradeOutcome };
36
+ //#endregion
@@ -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
@@ -444,6 +444,14 @@ var GatewayProtocolRequestError = class extends Error {
444
444
  this.retryAfterMs = error.retryAfterMs;
445
445
  }
446
446
  };
447
+ /** Preserve response metadata after custom factories without adding it to error JSON. */
448
+ function retainGatewayResponsePayload(error, payload) {
449
+ Object.defineProperty(error, "responsePayload", {
450
+ value: payload,
451
+ enumerable: false,
452
+ configurable: true
453
+ });
454
+ }
447
455
  /** A local transport deadline, distinct from a Gateway's authoritative rejection. */
448
456
  var GatewayProtocolRequestTimeoutError = class extends Error {
449
457
  constructor(params, message = `gateway request timed out after ${params.timeoutMs}ms: ${params.method}`) {
@@ -462,8 +470,7 @@ var GatewayPendingRequests = class {
462
470
  constructor(opts) {
463
471
  this.opts = opts;
464
472
  this.pending = /* @__PURE__ */ new Map();
465
- this.retiredIds = /* @__PURE__ */ new Set();
466
- this.collisionSuffix = 0;
473
+ this.requestSequence = 0;
467
474
  }
468
475
  get hasPending() {
469
476
  return this.pending.size > 0;
@@ -500,7 +507,6 @@ var GatewayPendingRequests = class {
500
507
  const retire = (errorCode) => {
501
508
  if (this.pending.get(id) !== pending) return false;
502
509
  this.pending.delete(id);
503
- this.retiredIds.add(id);
504
510
  cleanup();
505
511
  this.finishTiming(id, pending, false, errorCode);
506
512
  return true;
@@ -546,7 +552,7 @@ var GatewayPendingRequests = class {
546
552
  const pending = this.pending.get(frame.id);
547
553
  if (!pending) return;
548
554
  const status = frame.payload?.status;
549
- if (pending.expectFinal && status === "accepted") {
555
+ if (frame.ok && pending.expectFinal && status === "accepted") {
550
556
  if (!pending.acceptedNotified) {
551
557
  pending.acceptedNotified = true;
552
558
  this.invoke("accepted", () => pending.onAccepted?.(frame.payload));
@@ -561,39 +567,41 @@ var GatewayPendingRequests = class {
561
567
  return;
562
568
  }
563
569
  this.finishTiming(frame.id, pending, false, frame.error?.code);
564
- pending.reject(this.opts.createRequestError?.(frame.error ?? {}) ?? new GatewayProtocolRequestError(frame.error ?? {}));
570
+ const error = this.opts.createRequestError?.(frame.error ?? {}) ?? new GatewayProtocolRequestError(frame.error ?? {});
571
+ retainGatewayResponsePayload(error, frame.payload);
572
+ pending.reject(error);
565
573
  }
566
574
  flush(error) {
567
- for (const [id, pending] of this.pending) {
568
- this.finishTiming(id, pending, false, "CLIENT_CLOSED");
575
+ const retired = [...this.pending];
576
+ this.pending.clear();
577
+ this.requestSequence = 0;
578
+ for (const [id, pending] of retired) {
569
579
  pending.cleanup?.();
580
+ this.finishTiming(id, pending, false, "CLIENT_CLOSED");
570
581
  pending.reject(error);
571
582
  }
572
- this.pending.clear();
573
- this.retiredIds.clear();
574
- this.collisionSuffix = 0;
575
583
  }
576
584
  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
+ this.requestSequence += 1;
586
+ return `${this.requestSequence}:${this.opts.createRequestId()}`;
585
587
  }
586
588
  finishTiming(id, pending, ok, errorCode) {
587
589
  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
- }));
590
+ try {
591
+ const onTiming = this.opts.onTiming;
592
+ if (onTiming === void 0 || onTiming === null) return;
593
+ Reflect.apply(onTiming, this.opts, [{
594
+ id,
595
+ method: pending.method,
596
+ ok,
597
+ durationMs: Math.max(0, endedAtMs - pending.startedAtMs),
598
+ startedAtMs: pending.startedAtMs,
599
+ endedAtMs,
600
+ errorCode
601
+ }]);
602
+ } catch (error) {
603
+ this.opts.onCallbackError?.("request timing", error);
604
+ }
597
605
  }
598
606
  invoke(label, callback) {
599
607
  try {
@@ -733,8 +741,12 @@ var GatewayProtocolClient = class {
733
741
  const normalized = error instanceof Error ? error : new Error(String(error));
734
742
  this.opts.onSocketFactoryError?.(normalized);
735
743
  this.opts.onConnectError?.(normalized);
736
- if (this.opts.rethrowSocketFactoryError?.(normalized)) throw normalized;
744
+ if (this.opts.rethrowSocketFactoryError?.(normalized)) {
745
+ if (this.generation > 0 && !this.stopped && !this.socket && !this.reconnectSignal) this.opts.onReconnectStopped?.(normalized);
746
+ throw normalized;
747
+ }
737
748
  if (this.opts.shouldRetrySocketFactoryError?.(normalized) && !this.stopped && !this.socket && !this.reconnectSignal) this.scheduleReconnect();
749
+ else if (this.generation > 0 && !this.stopped && !this.socket && !this.reconnectSignal) this.opts.onReconnectStopped?.(normalized);
738
750
  return;
739
751
  }
740
752
  this.generation = generation;
@@ -823,7 +835,7 @@ var GatewayProtocolClient = class {
823
835
  this.recordTiming("request-sent", generation, plan);
824
836
  this.connectRequestSent = true;
825
837
  this.request("connect", this.opts.buildConnectParams(plan)).then((hello) => {
826
- if (!this.isActive(socket, generation)) return;
838
+ if (!this.isActive(socket, generation) || !socket.isOpen()) return;
827
839
  this.helloReceived = true;
828
840
  this.clearHandshakeTimer();
829
841
  this.connectFailure = void 0;
@@ -927,7 +939,7 @@ var GatewayProtocolClient = class {
927
939
  const decision = this.opts.resolveClose(context);
928
940
  this.requests.flush(decision.pendingError ?? context.connectFailure?.error ?? /* @__PURE__ */ new Error(`gateway closed (${code}): ${reason}`));
929
941
  this.invoke("close", () => this.opts.onClose?.(context, decision));
930
- if (decision.retry && !this.stopped) this.scheduleReconnect(decision.reconnectDelayMs ?? context.connectFailure?.reconnectDelayMs);
942
+ if (decision.retry && !this.stopped && !this.socket && !this.reconnectSignal) this.scheduleReconnect(decision.reconnectDelayMs ?? context.connectFailure?.reconnectDelayMs);
931
943
  }
932
944
  handleSocketError(socket, generation, error) {
933
945
  if (!this.isActive(socket, generation) || this.connectSent) return;
@@ -942,7 +954,7 @@ var GatewayProtocolClient = class {
942
954
  sleepWithAbort(retry.delayMs, retry.signal).then(() => {
943
955
  if (this.reconnectSignal !== retry.signal) return;
944
956
  this.reconnectSignal = null;
945
- this.connect();
957
+ this.invoke("reconnect", () => this.connect());
946
958
  }, () => {
947
959
  if (this.reconnectSignal === retry.signal) this.reconnectSignal = null;
948
960
  });
@@ -983,6 +995,7 @@ const NON_RECOVERABLE_AUTH_ERRORS = /* @__PURE__ */ new Set([
983
995
  ConnectErrorDetailCodes.AUTH_RATE_LIMITED,
984
996
  ConnectErrorDetailCodes.AUTH_DEVICE_TOKEN_MISMATCH,
985
997
  ConnectErrorDetailCodes.AUTH_SCOPE_MISMATCH,
998
+ ConnectErrorDetailCodes.AUTH_IDENTITY_HEADER_REQUIRED,
986
999
  ConnectErrorDetailCodes.CONTROL_UI_BUILD_MISMATCH,
987
1000
  ConnectErrorDetailCodes.PAIRING_REQUIRED,
988
1001
  ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED,
@@ -994,15 +1007,74 @@ function shouldPauseGatewayReconnect(params) {
994
1007
  const pairing = readPairingConnectErrorDetails(params.details);
995
1008
  if (code === ConnectErrorDetailCodes.PAIRING_REQUIRED && (pairing?.pauseReconnect === false || pairing?.recommendedNextStep === "wait_then_retry")) return false;
996
1009
  if (code === ConnectErrorDetailCodes.AUTH_TOKEN_MISMATCH) return params.tokenMismatchIsTerminal === true && !params.deviceTokenRetryPending;
1010
+ if (code === ConnectErrorDetailCodes.AUTH_IDENTITY_HEADER_REQUIRED) return !params.deviceTokenRetryPending;
997
1011
  return NON_RECOVERABLE_AUTH_ERRORS.has(code) || params.protocolMismatchIsTerminal === true && code === ConnectErrorDetailCodes.PROTOCOL_MISMATCH || params.clientVersionMismatchIsTerminal === true && code === ConnectErrorDetailCodes.CLIENT_VERSION_MISMATCH;
998
1012
  }
999
1013
  //#endregion
1014
+ //#region packages/normalization-core/src/record-coerce.ts
1015
+ /** Type guard for non-array object records at browser-safe boundaries. */
1016
+ function isRecord(value) {
1017
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1018
+ }
1019
+ /** Returns a non-array record or null. */
1020
+ function asNullableRecord(value) {
1021
+ return isRecord(value) ? value : null;
1022
+ }
1023
+ //#endregion
1024
+ //#region packages/gateway-client/src/session-projection-message-identity.ts
1025
+ function readSessionProjectionString(value) {
1026
+ return typeof value === "string" ? value.trim() || null : null;
1027
+ }
1028
+ function readPositiveSafeInteger(value) {
1029
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
1030
+ }
1031
+ /** History and status markers carry transcript order even when they have no chat role. */
1032
+ function readSessionMessageSequence(message, envelope) {
1033
+ return readPositiveSafeInteger(asNullableRecord(asNullableRecord(message)?.["__openclaw"])?.seq) ?? readPositiveSafeInteger(envelope?.messageSeq);
1034
+ }
1035
+ /** Run ownership normalizes a user-turn suffix without changing its persisted send key. */
1036
+ function normalizeSessionProjectionRunId(value) {
1037
+ const runId = readSessionProjectionString(value);
1038
+ return runId?.endsWith(":user") ? runId.slice(0, -5) || null : runId;
1039
+ }
1040
+ /** Persisted row facts win; assistant run ownership comes from its authoritative producer. */
1041
+ function readSessionMessageIdentity(message, envelope) {
1042
+ const record = asNullableRecord(message);
1043
+ const role = readSessionProjectionString(record?.role)?.toLowerCase();
1044
+ if (!record || !role) return null;
1045
+ const metadata = asNullableRecord(record["__openclaw"]);
1046
+ const importedFrom = readSessionProjectionString(metadata?.importedFrom);
1047
+ const cliSessionId = readSessionProjectionString(metadata?.cliSessionId);
1048
+ const externalId = readSessionProjectionString(metadata?.externalId);
1049
+ const idempotencyKey = readSessionProjectionString(metadata?.idempotencyKey) ?? readSessionProjectionString(record.idempotencyKey) ?? readSessionProjectionString(envelope?.idempotencyKey) ?? readSessionProjectionString(envelope?.clientRunId);
1050
+ const persistedRunId = normalizeSessionProjectionRunId(idempotencyKey);
1051
+ const envelopeRunId = normalizeSessionProjectionRunId(envelope?.runId);
1052
+ const metadataRunId = normalizeSessionProjectionRunId(metadata?.runId);
1053
+ const mirroredMessage = readSessionProjectionString(metadata?.mirrorOrigin) !== null;
1054
+ const isCliAssistant = role === "assistant" && readSessionProjectionString(record.api)?.toLowerCase() === "cli";
1055
+ const canonicalPersistedRunId = isCliAssistant && persistedRunId?.startsWith("cli-assistant:") ? readSessionProjectionString(persistedRunId.slice(14)) : persistedRunId;
1056
+ const optimisticRunId = metadata && Object.keys(metadata).every((key) => key === "idempotencyKey") ? canonicalPersistedRunId : null;
1057
+ return {
1058
+ role,
1059
+ id: readSessionProjectionString(metadata?.id) ?? readSessionProjectionString(envelope?.messageId),
1060
+ sequence: readSessionMessageSequence(message, envelope),
1061
+ idempotencyKey,
1062
+ runId: role === "assistant" ? metadataRunId ?? envelopeRunId ?? (isCliAssistant || !mirroredMessage ? canonicalPersistedRunId : null) ?? optimisticRunId : metadataRunId ?? canonicalPersistedRunId ?? envelopeRunId,
1063
+ isImported: Boolean(importedFrom || cliSessionId || externalId),
1064
+ externalSource: importedFrom && cliSessionId && externalId ? JSON.stringify([
1065
+ importedFrom,
1066
+ cliSessionId,
1067
+ externalId
1068
+ ]) : null
1069
+ };
1070
+ }
1071
+ //#endregion
1000
1072
  //#region packages/gateway-client/src/session-projection-run-event.ts
1001
- function readNonemptyString$1(value) {
1073
+ function readNonemptyString(value) {
1002
1074
  return typeof value === "string" ? value.trim() || null : null;
1003
1075
  }
1004
1076
  function reduceSessionProjectionRunEventImpl(projection, event, scope = {}) {
1005
- const runId = readNonemptyString$1(event.runId);
1077
+ const runId = readNonemptyString(event.runId);
1006
1078
  if (!runId || typeof event.state !== "string" || ![
1007
1079
  "delta",
1008
1080
  "final",
@@ -1010,9 +1082,9 @@ function reduceSessionProjectionRunEventImpl(projection, event, scope = {}) {
1010
1082
  "aborted"
1011
1083
  ].includes(event.state)) return null;
1012
1084
  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);
1085
+ const messageStopReason = isRecord(message) ? readNonemptyString(message.stopReason) : null;
1086
+ const stopReason = readNonemptyString(event.stopReason) ?? messageStopReason;
1087
+ const errorKind = readNonemptyString(event.errorKind);
1016
1088
  const base = {
1017
1089
  runId,
1018
1090
  ...message === void 0 ? {} : { message },
@@ -1047,53 +1119,11 @@ const SESSION_PROJECTION_SCOPE_KEYS = [
1047
1119
  "lifecycleRevision",
1048
1120
  "activeLeafEntryId"
1049
1121
  ];
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
1122
  /** Local turns have no durable transcript metadata beyond their own optional send key. */
1093
1123
  function isLocallyOptimisticSessionMessage(message) {
1094
1124
  const identity = readSessionMessageIdentity(message);
1095
1125
  if (!identity || identity.role !== "user" && identity.role !== "assistant") return false;
1096
- const metadata = readRecord(readRecord(message)?.["__openclaw"]);
1126
+ const metadata = asNullableRecord(asNullableRecord(message)?.["__openclaw"]);
1097
1127
  return !metadata || Object.keys(metadata).every((key) => key === "idempotencyKey");
1098
1128
  }
1099
1129
  function createEntry(message, options) {
@@ -1153,10 +1183,12 @@ function entryMatches(left, right, allowSnapshotPromotion = false) {
1153
1183
  if (sameTranscriptIdentity(left.identity, right.identity)) return true;
1154
1184
  const durableEntry = left.identity?.id ? left : right.identity?.id ? right : null;
1155
1185
  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;
1186
+ const durableMetadata = asNullableRecord(asNullableRecord(durableEntry?.message)?.["__openclaw"]);
1187
+ 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
1188
  const persisted = left.identity;
1158
1189
  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;
1190
+ const persistedMetadata = asNullableRecord(asNullableRecord(left.message)?.["__openclaw"]);
1191
+ 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
1192
  if (left.pending && right.pending) return Boolean(left.identity?.role === right.identity?.role && left.pendingRunId && left.pendingRunId === right.pendingRunId);
1161
1193
  const pending = left.pending ? left : right.pending ? right : null;
1162
1194
  const authoritative = pending === left ? right : pending === right ? left : null;
@@ -1175,10 +1207,24 @@ function insertEntry(entries, incoming, runs) {
1175
1207
  const candidate = entry.identity?.sequence;
1176
1208
  return candidate !== void 0 && candidate !== null && candidate > sequence;
1177
1209
  });
1178
- if (nextIndex < 0 && incoming.identity?.role === "user" && incoming.identity.runId) {
1210
+ if (sequence !== void 0 && sequence !== null && nextIndex < 0) nextIndex = entries.length;
1211
+ if (incoming.identity?.role === "user" && incoming.identity.runId) {
1179
1212
  const runId = incoming.identity.runId;
1180
1213
  const terminalMessage = runs?.[runId]?.message;
1181
- nextIndex = entries.findIndex((entry) => entry.identity?.role === "assistant" && (entry.identity.runId === runId || entry.message === terminalMessage));
1214
+ const belongsToRun = (entry) => entry.identity?.role === "assistant" && (entry.identity.runId === runId || entry.message === terminalMessage);
1215
+ if (sequence !== void 0 && sequence !== null) {
1216
+ const before = [];
1217
+ const replies = [];
1218
+ for (const entry of entries.slice(0, nextIndex)) (entry.identity?.sequence === null && belongsToRun(entry) ? replies : before).push(entry);
1219
+ return [
1220
+ ...before,
1221
+ incoming,
1222
+ ...replies,
1223
+ ...entries.slice(nextIndex)
1224
+ ];
1225
+ }
1226
+ const terminalIndex = entries.findIndex(belongsToRun);
1227
+ if (terminalIndex >= 0 && (nextIndex < 0 || terminalIndex < nextIndex)) nextIndex = terminalIndex;
1182
1228
  }
1183
1229
  return nextIndex < 0 ? [...entries, incoming] : [
1184
1230
  ...entries.slice(0, nextIndex),
@@ -1197,7 +1243,8 @@ function projectLiveSessionMessage(state, message, envelope, scope = {}) {
1197
1243
  if (existingIndex < 0) return withEntries(state, insertEntry(state.entries, incoming, state.runs));
1198
1244
  const existing = state.entries[existingIndex];
1199
1245
  if (existing && existing.message === message && existing.live && !existing.pending) return state;
1200
- if (existing?.pending && incoming.identity.sequence !== null) {
1246
+ if (existing && !existing.pending && existing.identity?.id && !incoming.identity.id) return state;
1247
+ if (existing && incoming.identity.sequence !== null && (existing.pending || existing.identity?.sequence === null)) {
1201
1248
  const sequence = incoming.identity.sequence;
1202
1249
  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));
1203
1250
  }
@@ -1227,13 +1274,13 @@ function reconcileSessionProjectionSnapshot(state, messages, scope = {}, options
1227
1274
  }
1228
1275
  function hasDisplayableSessionMessage(message) {
1229
1276
  if (typeof message === "string") return message.trim().length > 0;
1230
- const record = readRecord(message);
1277
+ const record = asNullableRecord(message);
1231
1278
  if (!record) return false;
1232
1279
  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;
1280
+ const entry = asNullableRecord(block);
1281
+ return entry ? entry.type !== "text" || readSessionProjectionString(entry.text) !== null : typeof block === "string" && block.trim().length > 0;
1235
1282
  });
1236
- const media = readRecord(record["__openclaw"])?.media;
1283
+ const media = asNullableRecord(record["__openclaw"])?.media;
1237
1284
  return Boolean(typeof record.content === "string" && record.content.trim() || displayableBlocks || Array.isArray(media) && media.length > 0);
1238
1285
  }
1239
1286
  function readSessionProjectionFinalMessageIdentity(message) {
@@ -1242,8 +1289,8 @@ function readSessionProjectionFinalMessageIdentity(message) {
1242
1289
  if (identity?.externalSource) return `import:${identity.role}:${identity.externalSource}`;
1243
1290
  if (identity?.id && !identity.isImported) return `id:${identity.role}:${identity.id}`;
1244
1291
  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"]);
1292
+ const record = asNullableRecord(message);
1293
+ const metadata = asNullableRecord(record?.["__openclaw"]);
1247
1294
  try {
1248
1295
  return `content:${JSON.stringify([
1249
1296
  identity?.role ?? "assistant",
@@ -1274,7 +1321,7 @@ function retainSessionProjectionRuns(runs) {
1274
1321
  return Object.fromEntries([...active, ...retainedTerminal]);
1275
1322
  }
1276
1323
  function updateRun(state, incoming) {
1277
- const incomingErrorMessage = readNonemptyString(incoming.errorMessage);
1324
+ const incomingErrorMessage = readSessionProjectionString(incoming.errorMessage);
1278
1325
  const normalizedIncoming = { ...incoming };
1279
1326
  if (incomingErrorMessage) normalizedIncoming.errorMessage = incomingErrorMessage;
1280
1327
  else delete normalizedIncoming.errorMessage;
@@ -1285,7 +1332,7 @@ function updateRun(state, incoming) {
1285
1332
  const canRecoverFinal = !hasDisplayableSessionMessage(current.message) || (current.acceptedFinalMessageIdentities?.length ?? 0) > 0;
1286
1333
  const acceptFinal = incomingIsFinal && (current.status === incoming.status || canRecoverFinal) && incomingFinalIdentity !== null && !hasSessionProjectionAcceptedFinal(current, incoming.message);
1287
1334
  const recoverMessage = acceptFinal && !hasDisplayableSessionMessage(current.message);
1288
- const recoverError = readNonemptyString(current.errorMessage) === null && incomingErrorMessage !== null;
1335
+ const recoverError = readSessionProjectionString(current.errorMessage) === null && incomingErrorMessage !== null;
1289
1336
  if (!acceptFinal && !recoverError) return state;
1290
1337
  const firstFinalIdentity = readSessionProjectionFinalMessageIdentity(current.message);
1291
1338
  const previousFinalIdentities = current.acceptedFinalMessageIdentities ?? (firstFinalIdentity ? [firstFinalIdentity] : []);
@@ -1433,7 +1480,7 @@ var GatewaySessionMessageSubscriptionCoordinator = class {
1433
1480
  if (this.#retired) throw new Error("Session message subscription belongs to a replaced Gateway connection");
1434
1481
  const existing = [...this.#entries].find((candidate) => candidate.agentId === agentId && (this.#areKeysEquivalent(candidate.key, normalizedKey) || [...candidate.requestedKeys].some((requestedKey) => this.#areKeysEquivalent(requestedKey, normalizedKey))));
1435
1482
  if (!existing) {
1436
- const provisional = [...this.#entries].find((candidate) => candidate.agentId === agentId && !candidate.canonicalSettled);
1483
+ const provisional = [...this.#entries].find((candidate) => candidate.agentId === agentId && !candidate.canonicalSettled && this.#couldShareCanonicalIdentity(candidate.key, normalizedKey));
1437
1484
  if (provisional) {
1438
1485
  await (provisional.plainFallback ?? provisional.ready).catch(() => void 0);
1439
1486
  continue;
@@ -1489,7 +1536,7 @@ var GatewaySessionMessageSubscriptionCoordinator = class {
1489
1536
  entry.release = tracked;
1490
1537
  return tracked;
1491
1538
  }
1492
- const tracked = this.#client.request("sessions.messages.unsubscribe", sessionSubscriptionParams(entry.key, entry.agentId)).then(() => {
1539
+ const tracked = this.#client.request("sessions.messages.unsubscribe", sessionSubscriptionParams(entry.key, entry.agentId), { timeoutMs: DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS }).then(() => {
1493
1540
  this.#finishRelease(subscription, owner, true);
1494
1541
  }).finally(() => {
1495
1542
  if (entry.release === tracked) entry.release = null;
@@ -1564,9 +1611,18 @@ var GatewaySessionMessageSubscriptionCoordinator = class {
1564
1611
  return upgrade;
1565
1612
  }
1566
1613
  async #requestSubscribe(entry, includeApprovals) {
1567
- const result = await this.#client.request("sessions.messages.subscribe", {
1568
- ...sessionSubscriptionParams(entry.key, entry.agentId),
1569
- ...includeApprovals ? { includeApprovals: true } : {}
1614
+ const params = sessionSubscriptionParams(entry.key, entry.agentId);
1615
+ const result = await this.#client.request("sessions.messages.subscribe", includeApprovals ? {
1616
+ ...params,
1617
+ includeApprovals: true
1618
+ } : params, { timeoutMs: DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS }).catch(async (error) => {
1619
+ if (!(error instanceof GatewayProtocolRequestTimeoutError) || !error.requestSent || this.#retired) throw error;
1620
+ try {
1621
+ await this.#client.request(entry.handles.size > 0 ? "sessions.messages.subscribe" : "sessions.messages.unsubscribe", params, { timeoutMs: DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS });
1622
+ } catch (recoveryError) {
1623
+ if (!this.#retired) throw new AggregateError([error, recoveryError], "session message subscription recovery failed", { cause: recoveryError });
1624
+ }
1625
+ throw error;
1570
1626
  });
1571
1627
  const response = result && typeof result === "object" ? result : null;
1572
1628
  const responseKey = response && "key" in response ? response.key : void 0;
@@ -1584,6 +1640,11 @@ var GatewaySessionMessageSubscriptionCoordinator = class {
1584
1640
  #areKeysEquivalent(left, right) {
1585
1641
  return left === right || this.#keysEquivalent?.(left, right) === true;
1586
1642
  }
1643
+ #couldShareCanonicalIdentity(left, right) {
1644
+ const leftBody = left.replace(/^agent:[^:]+:/i, "").toLowerCase();
1645
+ const rightBody = right.replace(/^agent:[^:]+:/i, "").toLowerCase();
1646
+ return leftBody === rightBody || leftBody === "main" || rightBody === "main" || leftBody === "global" || rightBody === "global";
1647
+ }
1587
1648
  };
1588
1649
  const sessionMessageSubscriptionOwners = /* @__PURE__ */ new WeakMap();
1589
1650
  const sessionMessageSubscriptionCoordinators = /* @__PURE__ */ new WeakMap();
@@ -1602,4 +1663,4 @@ function releaseGatewaySessionMessageSubscription(subscription) {
1602
1663
  return sessionMessageSubscriptionOwners.get(subscription)?.coordinator.release(subscription) ?? Promise.resolve();
1603
1664
  }
1604
1665
  //#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 };
1666
+ 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 };