@openclaw/gateway-client 2026.7.2-beta.4 → 2026.7.2-beta.6

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,3 +1,4 @@
1
+ import { clearGatewayConnectTimeout, startGatewayConnectTimeout } from "./timeouts.mjs";
1
2
  import { ConnectErrorDetailCodes, readConnectErrorDetailCode, readConnectErrorRecoveryAdvice, readPairingConnectErrorDetails } from "@openclaw/gateway-protocol/connect-error-details";
2
3
  import { isGatewayEventFrame, isGatewayResponseFrame } from "@openclaw/gateway-protocol/frame-guards";
3
4
  //#region packages/gateway-client/src/device-auth.ts
@@ -381,6 +382,27 @@ function createRetryRunner(runtime = {}) {
381
382
  }
382
383
  createRetryRunner();
383
384
  //#endregion
385
+ //#region packages/gateway-client/src/event-listeners.ts
386
+ /** Subscription identity prevents old frames and disposers from reviving callbacks. */
387
+ var GatewayEventListeners = class {
388
+ constructor() {
389
+ this.listeners = /* @__PURE__ */ new Map();
390
+ }
391
+ add(listener) {
392
+ const subscription = this.listeners.get(listener) ?? {};
393
+ this.listeners.set(listener, subscription);
394
+ return () => {
395
+ if (this.listeners.get(listener) === subscription) this.listeners.delete(listener);
396
+ };
397
+ }
398
+ snapshot() {
399
+ return [...this.listeners];
400
+ }
401
+ isCurrent(listener, subscription) {
402
+ return this.listeners.get(listener) === subscription;
403
+ }
404
+ };
405
+ //#endregion
384
406
  //#region packages/gateway-client/src/protocol-client.ts
385
407
  var GatewayProtocolRequestError = class extends Error {
386
408
  constructor(error) {
@@ -402,7 +424,7 @@ var GatewayProtocolClient = class {
402
424
  this.opts = opts;
403
425
  this.socket = null;
404
426
  this.pending = /* @__PURE__ */ new Map();
405
- this.listeners = /* @__PURE__ */ new Set();
427
+ this.listeners = new GatewayEventListeners();
406
428
  this.stopped = true;
407
429
  this.generation = 0;
408
430
  this.lastSeq = null;
@@ -410,6 +432,7 @@ var GatewayProtocolClient = class {
410
432
  this.connectSent = false;
411
433
  this.connectRequestSent = false;
412
434
  this.handshakeTimer = null;
435
+ this.reconnectSignal = null;
413
436
  this.socketOpened = false;
414
437
  this.helloReceived = false;
415
438
  this.connectTiming = null;
@@ -433,6 +456,7 @@ var GatewayProtocolClient = class {
433
456
  return [...this.pending.values()].some((pending) => pending.unbounded);
434
457
  }
435
458
  start() {
459
+ if (this.socket || this.reconnectSignal) return;
436
460
  this.stopped = false;
437
461
  this.reconnectSupervisor.cancel();
438
462
  this.connect();
@@ -440,6 +464,7 @@ var GatewayProtocolClient = class {
440
464
  stop() {
441
465
  this.stopped = true;
442
466
  this.clearHandshakeTimer();
467
+ this.reconnectSignal = null;
443
468
  this.reconnectSupervisor.reset();
444
469
  const socket = this.socket;
445
470
  if (socket && this.opts.notifyStoppedClose) this.stoppedSocket = {
@@ -513,13 +538,13 @@ var GatewayProtocolClient = class {
513
538
  });
514
539
  }
515
540
  addEventListener(listener) {
516
- this.listeners.add(listener);
517
- return () => this.listeners.delete(listener);
541
+ return this.listeners.add(listener);
518
542
  }
519
543
  closeSocket(code, reason) {
520
544
  this.socket?.close(code, reason);
521
545
  }
522
546
  resetReconnectBackoff(initialMs) {
547
+ this.reconnectSignal = null;
523
548
  this.reconnectSupervisor.reset(initialMs);
524
549
  }
525
550
  recordTiming(phase, generation, plan, detail) {
@@ -544,9 +569,9 @@ var GatewayProtocolClient = class {
544
569
  connect() {
545
570
  if (this.stopped) return;
546
571
  const generation = this.generation + 1;
572
+ this.lastSeq = null;
547
573
  this.connectNonce = null;
548
- this.connectSent = false;
549
- this.connectRequestSent = false;
574
+ this.connectSent = this.connectRequestSent = false;
550
575
  this.socketOpened = false;
551
576
  this.helloReceived = false;
552
577
  this.connectFailure = void 0;
@@ -563,6 +588,7 @@ var GatewayProtocolClient = class {
563
588
  this.opts.onSocketFactoryError?.(normalized);
564
589
  this.opts.onConnectError?.(normalized);
565
590
  if (this.opts.rethrowSocketFactoryError?.(normalized)) throw normalized;
591
+ if (this.opts.shouldRetrySocketFactoryError?.(normalized) && !this.stopped && !this.socket && !this.reconnectSignal) this.scheduleReconnect();
566
592
  return;
567
593
  }
568
594
  this.generation = generation;
@@ -608,6 +634,9 @@ var GatewayProtocolClient = class {
608
634
  if (!this.isActive(socket, generation) || !socket.isOpen() || this.connectSent) return;
609
635
  this.connectSent = true;
610
636
  this.clearHandshakeTimer();
637
+ this.handshakeTimer = startGatewayConnectTimeout(() => {
638
+ if (this.isActive(socket, generation) && !this.helloReceived) socket.close(4e3, "connect timeout");
639
+ });
611
640
  let planOrPromise;
612
641
  try {
613
642
  planOrPromise = this.opts.buildConnectPlan({
@@ -648,6 +677,7 @@ var GatewayProtocolClient = class {
648
677
  this.request("connect", this.opts.buildConnectParams(plan)).then((hello) => {
649
678
  if (!this.isActive(socket, generation)) return;
650
679
  this.helloReceived = true;
680
+ this.clearHandshakeTimer();
651
681
  this.connectFailure = void 0;
652
682
  this.reconnectSupervisor.reset();
653
683
  this.recordTiming("hello", generation, plan);
@@ -703,11 +733,16 @@ var GatewayProtocolClient = class {
703
733
  expected,
704
734
  received: seq
705
735
  }));
736
+ if (!this.isActive(socket, generation)) return;
706
737
  }
707
738
  this.lastSeq = seq;
708
739
  }
740
+ const listeners = this.listeners.snapshot();
709
741
  this.invoke("event", () => this.opts.onEvent?.(parsed));
710
- for (const listener of this.listeners) this.invoke("event listener", () => listener(parsed));
742
+ for (const [listener, subscription] of listeners) {
743
+ if (!this.isActive(socket, generation)) return;
744
+ if (this.listeners.isCurrent(listener, subscription)) this.invoke("event listener", () => listener(parsed));
745
+ }
711
746
  return;
712
747
  }
713
748
  if (!isGatewayResponseFrame(parsed)) return;
@@ -793,7 +828,14 @@ var GatewayProtocolClient = class {
793
828
  if (overrideMs !== void 0) this.reconnectSupervisor.nextDelayOverrideMs = overrideMs;
794
829
  const retry = this.reconnectSupervisor.next();
795
830
  if (!retry) return;
796
- sleepWithAbort(retry.delayMs, retry.signal).then(() => this.connect(), () => {});
831
+ this.reconnectSignal = retry.signal;
832
+ sleepWithAbort(retry.delayMs, retry.signal).then(() => {
833
+ if (this.reconnectSignal !== retry.signal) return;
834
+ this.reconnectSignal = null;
835
+ this.connect();
836
+ }, () => {
837
+ if (this.reconnectSignal === retry.signal) this.reconnectSignal = null;
838
+ });
797
839
  }
798
840
  closeContext() {
799
841
  return {
@@ -811,10 +853,7 @@ var GatewayProtocolClient = class {
811
853
  return this.opts.nowMs?.() ?? Date.now();
812
854
  }
813
855
  clearHandshakeTimer() {
814
- if (this.handshakeTimer) {
815
- clearTimeout(this.handshakeTimer);
816
- this.handshakeTimer = null;
817
- }
856
+ this.handshakeTimer = clearGatewayConnectTimeout(this.handshakeTimer);
818
857
  }
819
858
  invoke(label, callback) {
820
859
  try {
@@ -847,4 +886,591 @@ function shouldPauseGatewayReconnect(params) {
847
886
  return NON_RECOVERABLE_AUTH_ERRORS.has(code) || params.protocolMismatchIsTerminal === true && code === ConnectErrorDetailCodes.PROTOCOL_MISMATCH || params.clientVersionMismatchIsTerminal === true && code === ConnectErrorDetailCodes.CLIENT_VERSION_MISMATCH;
848
887
  }
849
888
  //#endregion
850
- export { buildGatewayConnectAuth as a, shouldRetryGatewayWithDeviceToken as c, normalizeDeviceMetadataForAuth as d, GatewayBrowserDeviceAuthLifecycle as i, buildDeviceAuthPayload as l, GatewayProtocolClient as n, resolveGatewayConnectScopes as o, GatewayProtocolRequestError as r, selectGatewayConnectAuth as s, shouldPauseGatewayReconnect as t, buildDeviceAuthPayloadV3 as u };
889
+ //#region packages/gateway-client/src/session-projection.ts
890
+ const MAX_TRACKED_SESSION_RUNS = 200;
891
+ const RETAINED_SESSION_RUNS = 150;
892
+ const SESSION_PROJECTION_SCOPE_KEYS = [
893
+ "sessionKey",
894
+ "sessionId",
895
+ "agentId",
896
+ "lifecycleRevision",
897
+ "activeLeafEntryId"
898
+ ];
899
+ function readRecord(value) {
900
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
901
+ }
902
+ function readNonemptyString(value) {
903
+ return typeof value === "string" ? value.trim() || null : null;
904
+ }
905
+ function readPositiveSafeInteger(value) {
906
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
907
+ }
908
+ /** History and status markers carry transcript order even when they have no chat role. */
909
+ function readSessionMessageSequence(message, envelope) {
910
+ return readPositiveSafeInteger(readRecord(readRecord(message)?.["__openclaw"])?.seq) ?? readPositiveSafeInteger(envelope?.messageSeq);
911
+ }
912
+ /** Run ownership normalizes a user-turn suffix without changing its persisted send key. */
913
+ function normalizeSessionProjectionRunId(value) {
914
+ const runId = readNonemptyString(value);
915
+ return runId?.endsWith(":user") ? runId.slice(0, -5) || null : runId;
916
+ }
917
+ /** Persisted transcript facts win over envelope projections and provider-local import IDs. */
918
+ function readSessionMessageIdentity(message, envelope) {
919
+ const record = readRecord(message);
920
+ const role = readNonemptyString(record?.role)?.toLowerCase();
921
+ if (!record || !role) return null;
922
+ const metadata = readRecord(record["__openclaw"]);
923
+ const importedFrom = readNonemptyString(metadata?.importedFrom);
924
+ const cliSessionId = readNonemptyString(metadata?.cliSessionId);
925
+ const externalId = readNonemptyString(metadata?.externalId);
926
+ const idempotencyKey = readNonemptyString(metadata?.idempotencyKey) ?? readNonemptyString(record.idempotencyKey) ?? readNonemptyString(envelope?.idempotencyKey) ?? readNonemptyString(envelope?.clientRunId);
927
+ return {
928
+ role,
929
+ id: readNonemptyString(metadata?.id) ?? readNonemptyString(envelope?.messageId),
930
+ sequence: readSessionMessageSequence(message, envelope),
931
+ idempotencyKey,
932
+ runId: normalizeSessionProjectionRunId(idempotencyKey) ?? normalizeSessionProjectionRunId(envelope?.runId),
933
+ isImported: Boolean(importedFrom || cliSessionId || externalId),
934
+ externalSource: importedFrom && cliSessionId && externalId ? JSON.stringify([
935
+ importedFrom,
936
+ cliSessionId,
937
+ externalId
938
+ ]) : null
939
+ };
940
+ }
941
+ /** Local turns have no durable transcript metadata beyond their own optional send key. */
942
+ function isLocallyOptimisticSessionMessage(message) {
943
+ const identity = readSessionMessageIdentity(message);
944
+ if (!identity || identity.role !== "user" && identity.role !== "assistant") return false;
945
+ const metadata = readRecord(readRecord(message)?.["__openclaw"]);
946
+ return !metadata || Object.keys(metadata).every((key) => key === "idempotencyKey");
947
+ }
948
+ function createEntry(message, options) {
949
+ const identity = readSessionMessageIdentity(message, options?.envelope);
950
+ const inferredPendingRunId = options?.live !== true && isLocallyOptimisticSessionMessage(message) ? identity?.runId : null;
951
+ const pendingRunId = normalizeSessionProjectionRunId(options?.pendingRunId ?? inferredPendingRunId);
952
+ return {
953
+ message,
954
+ identity,
955
+ live: options?.live === true,
956
+ pending: pendingRunId !== null,
957
+ pendingRunId
958
+ };
959
+ }
960
+ function createProjectionEntries(messages) {
961
+ let pendingUserRunId = null;
962
+ return messages.map((message) => {
963
+ const entry = createEntry(message);
964
+ if (entry.identity?.role === "user") {
965
+ pendingUserRunId = entry.pending ? entry.pendingRunId : null;
966
+ return entry;
967
+ }
968
+ if (pendingUserRunId && entry.identity?.role === "assistant" && !entry.pending && isLocallyOptimisticSessionMessage(message)) return createEntry(message, { pendingRunId: pendingUserRunId });
969
+ if (!isLocallyOptimisticSessionMessage(message)) pendingUserRunId = null;
970
+ return entry;
971
+ });
972
+ }
973
+ function createSessionProjection(scope = {}, messages = []) {
974
+ const entries = createProjectionEntries(messages);
975
+ return {
976
+ scope: { ...scope },
977
+ entries,
978
+ messages: entries.map((entry) => entry.message),
979
+ runs: {},
980
+ hasTransportGap: false
981
+ };
982
+ }
983
+ function scopesMatch(left, right) {
984
+ return SESSION_PROJECTION_SCOPE_KEYS.every((key) => left[key] === void 0 || right[key] === void 0 || left[key] === right[key]);
985
+ }
986
+ function readEventScope(event) {
987
+ const scope = { ...event.scope };
988
+ for (const key of SESSION_PROJECTION_SCOPE_KEYS) if (event[key] !== void 0) Object.assign(scope, { [key]: event[key] });
989
+ return scope;
990
+ }
991
+ function sameTranscriptIdentity(left, right) {
992
+ if (!left || !right || left.role !== right.role) return false;
993
+ if (left.isImported || right.isImported) {
994
+ if (!left.isImported || !right.isImported) return false;
995
+ if (left.externalSource || right.externalSource) return Boolean(left.externalSource && left.externalSource === right.externalSource);
996
+ return left.sequence !== null && right.sequence !== null && left.sequence === right.sequence;
997
+ }
998
+ if (left.id || right.id) return Boolean(left.id && right.id && left.id === right.id);
999
+ return left.sequence !== null && right.sequence !== null && left.sequence === right.sequence;
1000
+ }
1001
+ function entryMatches(left, right, allowSnapshotPromotion = false) {
1002
+ if (sameTranscriptIdentity(left.identity, right.identity)) return true;
1003
+ const persisted = left.identity;
1004
+ const observed = right.identity;
1005
+ if (allowSnapshotPromotion && right.live && persisted && observed && persisted.role === observed.role && !persisted.isImported && !observed.isImported && persisted.id && !observed.id && (persisted.sequence !== null && persisted.sequence === observed.sequence || persisted.role === "assistant" && observed.sequence === null && persisted.runId !== null && persisted.runId === observed.runId)) return true;
1006
+ if (left.pending && right.pending) return Boolean(left.identity?.role === right.identity?.role && left.pendingRunId && left.pendingRunId === right.pendingRunId);
1007
+ const pending = left.pending ? left : right.pending ? right : null;
1008
+ const authoritative = pending === left ? right : pending === right ? left : null;
1009
+ return Boolean(pending && authoritative && pending.identity?.role === authoritative.identity?.role && !pending.identity?.isImported && !authoritative.identity?.isImported && pending.pendingRunId && pending.pendingRunId === authoritative.identity?.runId);
1010
+ }
1011
+ function withEntries(state, entries) {
1012
+ return {
1013
+ ...state,
1014
+ entries,
1015
+ messages: entries.map((entry) => entry.message)
1016
+ };
1017
+ }
1018
+ function insertEntry(entries, incoming, runs) {
1019
+ const sequence = incoming.identity?.sequence;
1020
+ let nextIndex = sequence === void 0 || sequence === null ? -1 : entries.findIndex((entry) => {
1021
+ const candidate = entry.identity?.sequence;
1022
+ return candidate !== void 0 && candidate !== null && candidate > sequence;
1023
+ });
1024
+ if (nextIndex < 0 && incoming.identity?.role === "user" && incoming.identity.runId) {
1025
+ const runId = incoming.identity.runId;
1026
+ const terminalMessage = runs?.[runId]?.message;
1027
+ nextIndex = entries.findIndex((entry) => entry.identity?.role === "assistant" && (entry.identity.runId === runId || entry.message === terminalMessage));
1028
+ }
1029
+ return nextIndex < 0 ? [...entries, incoming] : [
1030
+ ...entries.slice(0, nextIndex),
1031
+ incoming,
1032
+ ...entries.slice(nextIndex)
1033
+ ];
1034
+ }
1035
+ function projectLiveSessionMessage(state, message, envelope, scope = {}) {
1036
+ if (!scopesMatch(state.scope, scope)) return state;
1037
+ const incoming = createEntry(message, {
1038
+ envelope,
1039
+ live: true
1040
+ });
1041
+ if (!incoming.identity) return state;
1042
+ const existingIndex = state.entries.findIndex((entry) => entryMatches(entry, incoming));
1043
+ if (existingIndex < 0) return withEntries(state, insertEntry(state.entries, incoming, state.runs));
1044
+ const existing = state.entries[existingIndex];
1045
+ if (existing && existing.message === message && existing.live && !existing.pending) return state;
1046
+ if (existing?.pending && incoming.identity.sequence !== null) {
1047
+ const sequence = incoming.identity.sequence;
1048
+ 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));
1049
+ }
1050
+ return withEntries(state, [
1051
+ ...state.entries.slice(0, existingIndex),
1052
+ incoming,
1053
+ ...state.entries.slice(existingIndex + 1)
1054
+ ]);
1055
+ }
1056
+ /** Only observed live events and this client's pending turns may survive an older snapshot. */
1057
+ function reconcileSessionProjectionSnapshot(state, messages, scope = {}, options = {}) {
1058
+ const visibleMessages = options.shouldIncludeMessage ? messages.filter(options.shouldIncludeMessage) : messages;
1059
+ if (!scopesMatch(state.scope, scope)) return createSessionProjection(scope, visibleMessages);
1060
+ let entries = createProjectionEntries(visibleMessages);
1061
+ for (const current of state.entries) {
1062
+ if (!current.live && !current.pending || options.shouldIncludeMessage?.(current.message) === false || entries.filter((entry) => entryMatches(entry, current, true)).length === 1) continue;
1063
+ entries = insertEntry(entries, current, state.runs);
1064
+ }
1065
+ return {
1066
+ ...withEntries(state, entries),
1067
+ scope: {
1068
+ ...state.scope,
1069
+ ...scope
1070
+ },
1071
+ hasTransportGap: false
1072
+ };
1073
+ }
1074
+ function hasDisplayableSessionMessage(message) {
1075
+ if (typeof message === "string") return message.trim().length > 0;
1076
+ const record = readRecord(message);
1077
+ if (!record) return false;
1078
+ const displayableBlocks = Array.isArray(record.content) && record.content.some((block) => {
1079
+ const entry = readRecord(block);
1080
+ return entry ? entry.type !== "text" || readNonemptyString(entry.text) !== null : typeof block === "string" && block.trim().length > 0;
1081
+ });
1082
+ const media = readRecord(record["__openclaw"])?.media;
1083
+ return Boolean(typeof record.content === "string" && record.content.trim() || displayableBlocks || Array.isArray(media) && media.length > 0);
1084
+ }
1085
+ function readSessionProjectionFinalMessageIdentity(message) {
1086
+ if (!hasDisplayableSessionMessage(message)) return null;
1087
+ const identity = readSessionMessageIdentity(message);
1088
+ if (identity?.externalSource) return `import:${identity.role}:${identity.externalSource}`;
1089
+ if (identity?.id && !identity.isImported) return `id:${identity.role}:${identity.id}`;
1090
+ if (identity?.sequence !== null && identity?.sequence !== void 0) return `seq:${identity.role}:${identity.sequence}`;
1091
+ const record = readRecord(message);
1092
+ const metadata = readRecord(record?.["__openclaw"]);
1093
+ try {
1094
+ return `content:${JSON.stringify([
1095
+ identity?.role ?? "assistant",
1096
+ typeof message === "string" ? message : record?.content ?? null,
1097
+ metadata?.media ?? null,
1098
+ identity?.isImported ? [
1099
+ metadata?.importedFrom ?? null,
1100
+ metadata?.cliSessionId ?? null,
1101
+ metadata?.externalId ?? null
1102
+ ] : null
1103
+ ])}`;
1104
+ } catch {
1105
+ return null;
1106
+ }
1107
+ }
1108
+ /** Replayed finals are recognized against this run's bounded canonical terminal history. */
1109
+ function hasSessionProjectionAcceptedFinal(run, message) {
1110
+ const identity = readSessionProjectionFinalMessageIdentity(message);
1111
+ return Boolean(identity && run && (run.acceptedFinalMessageIdentities?.includes(identity) || readSessionProjectionFinalMessageIdentity(run.message) === identity));
1112
+ }
1113
+ function retainSessionProjectionRuns(runs) {
1114
+ const entries = Object.entries(runs);
1115
+ if (entries.length <= MAX_TRACKED_SESSION_RUNS) return runs;
1116
+ const active = entries.filter(([, run]) => run.status === "streaming");
1117
+ const terminal = entries.filter(([, run]) => run.status !== "streaming");
1118
+ const terminalLimit = Math.max(0, RETAINED_SESSION_RUNS - active.length);
1119
+ const retainedTerminal = terminalLimit > 0 ? terminal.slice(-terminalLimit) : [];
1120
+ return Object.fromEntries([...active, ...retainedTerminal]);
1121
+ }
1122
+ function updateRun(state, incoming) {
1123
+ const incomingErrorMessage = readNonemptyString(incoming.errorMessage);
1124
+ const normalizedIncoming = { ...incoming };
1125
+ if (incomingErrorMessage) normalizedIncoming.errorMessage = incomingErrorMessage;
1126
+ else delete normalizedIncoming.errorMessage;
1127
+ const current = state.runs[incoming.runId];
1128
+ if (current && current.status !== "streaming") {
1129
+ const incomingFinalIdentity = readSessionProjectionFinalMessageIdentity(incoming.message);
1130
+ const incomingIsFinal = incoming.status === "completed" || incoming.status === "yielded";
1131
+ const canRecoverFinal = !hasDisplayableSessionMessage(current.message) || (current.acceptedFinalMessageIdentities?.length ?? 0) > 0;
1132
+ const acceptFinal = incomingIsFinal && (current.status === incoming.status || canRecoverFinal) && incomingFinalIdentity !== null && !hasSessionProjectionAcceptedFinal(current, incoming.message);
1133
+ const recoverMessage = acceptFinal && !hasDisplayableSessionMessage(current.message);
1134
+ const recoverError = readNonemptyString(current.errorMessage) === null && incomingErrorMessage !== null;
1135
+ if (!acceptFinal && !recoverError) return state;
1136
+ const firstFinalIdentity = readSessionProjectionFinalMessageIdentity(current.message);
1137
+ const previousFinalIdentities = current.acceptedFinalMessageIdentities ?? (firstFinalIdentity ? [firstFinalIdentity] : []);
1138
+ return {
1139
+ ...state,
1140
+ runs: {
1141
+ ...state.runs,
1142
+ [incoming.runId]: {
1143
+ ...current,
1144
+ ...recoverMessage ? { message: incoming.message } : {},
1145
+ ...acceptFinal && incomingFinalIdentity ? { acceptedFinalMessageIdentities: [...previousFinalIdentities, incomingFinalIdentity].slice(-32) } : {},
1146
+ ...recoverError && incomingErrorMessage ? {
1147
+ errorMessage: incomingErrorMessage,
1148
+ ...incoming.errorKind ? { errorKind: incoming.errorKind } : {}
1149
+ } : {}
1150
+ }
1151
+ }
1152
+ };
1153
+ }
1154
+ const previousRuns = current && current.status === "streaming" && incoming.status !== "streaming" ? Object.fromEntries(Object.entries(state.runs).filter(([runId]) => runId !== incoming.runId)) : state.runs;
1155
+ const acceptedFinalIdentity = incoming.status === "completed" || incoming.status === "yielded" ? readSessionProjectionFinalMessageIdentity(incoming.message) : null;
1156
+ return {
1157
+ ...state,
1158
+ runs: retainSessionProjectionRuns({
1159
+ ...previousRuns,
1160
+ [incoming.runId]: {
1161
+ ...current,
1162
+ ...normalizedIncoming,
1163
+ ...acceptedFinalIdentity ? { acceptedFinalMessageIdentities: [acceptedFinalIdentity] } : {},
1164
+ ...incoming.message === void 0 && current?.message !== void 0 ? { message: current.message } : {}
1165
+ }
1166
+ })
1167
+ };
1168
+ }
1169
+ /** Reduces durable events, snapshots, and transport lifecycle without client-specific policy. */
1170
+ function reduceSessionProjection(state, event) {
1171
+ const scope = readEventScope(event);
1172
+ if (event.type === "snapshotLoaded") return scopesMatch(state.scope, scope) ? reconcileSessionProjectionSnapshot(state, event.messages, scope, event.options) : state;
1173
+ if (event.type === "sessionReset") {
1174
+ const { sessionKey, sessionId, agentId } = state.scope;
1175
+ return scopesMatch({
1176
+ sessionKey,
1177
+ sessionId,
1178
+ agentId
1179
+ }, scope) ? createSessionProjection({
1180
+ ...state.scope,
1181
+ ...scope
1182
+ }) : state;
1183
+ }
1184
+ if (!scopesMatch(state.scope, scope)) return state;
1185
+ switch (event.type) {
1186
+ case "messagePersisted": return projectLiveSessionMessage(state, event.message, event.envelope ?? event, scope);
1187
+ case "sendPending": {
1188
+ const pendingRunId = normalizeSessionProjectionRunId(event.idempotencyKey ?? event.runId);
1189
+ const incoming = createEntry(event.message, { pendingRunId });
1190
+ if (!pendingRunId || !incoming.identity) return state;
1191
+ return state.entries.findIndex((entry) => entryMatches(entry, incoming)) < 0 ? withEntries(state, insertEntry(state.entries, incoming, state.runs)) : state;
1192
+ }
1193
+ case "sendAcknowledged": {
1194
+ const runId = normalizeSessionProjectionRunId(event.idempotencyKey ?? event.runId);
1195
+ const previousRunId = normalizeSessionProjectionRunId(event.previousRunId);
1196
+ if (!runId || !previousRunId || previousRunId === runId) return state;
1197
+ let changed = false;
1198
+ const entries = state.entries.flatMap((entry) => {
1199
+ if (!entry.pending || entry.pendingRunId !== previousRunId) return [entry];
1200
+ changed = true;
1201
+ const rekeyed = {
1202
+ ...entry,
1203
+ pendingRunId: runId
1204
+ };
1205
+ return state.entries.some((candidate) => !candidate.pending && entryMatches(rekeyed, candidate)) ? [] : [rekeyed];
1206
+ });
1207
+ return changed ? withEntries(state, entries) : state;
1208
+ }
1209
+ case "sendFailed": {
1210
+ const runId = normalizeSessionProjectionRunId(event.runId);
1211
+ const entries = state.entries.filter((entry) => !entry.pending || entry.pendingRunId !== runId);
1212
+ return entries.length === state.entries.length ? state : withEntries(state, entries);
1213
+ }
1214
+ case "runDelta": return updateRun(state, {
1215
+ runId: event.runId,
1216
+ status: "streaming",
1217
+ ...event.message === void 0 ? {} : { message: event.message }
1218
+ });
1219
+ case "runTerminal": return updateRun(state, {
1220
+ runId: event.runId,
1221
+ status: event.status,
1222
+ ...event.message === void 0 ? {} : { message: event.message },
1223
+ ...event.stopReason === void 0 ? {} : { stopReason: event.stopReason },
1224
+ ...event.errorKind === void 0 ? {} : { errorKind: event.errorKind },
1225
+ ...event.errorMessage === void 0 ? {} : { errorMessage: event.errorMessage }
1226
+ });
1227
+ case "transportGap": return state.hasTransportGap ? state : {
1228
+ ...state,
1229
+ hasTransportGap: true
1230
+ };
1231
+ case "reconnected": return state;
1232
+ default: return state;
1233
+ }
1234
+ }
1235
+ /** Normalizes Gateway run envelopes once for every browser and terminal adapter. */
1236
+ function reduceSessionProjectionRunEvent(projection, event, scope = {}) {
1237
+ const runId = readNonemptyString(event.runId);
1238
+ const eventState = event.state;
1239
+ if (!runId || typeof eventState !== "string" || ![
1240
+ "delta",
1241
+ "final",
1242
+ "error",
1243
+ "aborted"
1244
+ ].includes(eventState)) return null;
1245
+ const message = event.message;
1246
+ const stopReason = readNonemptyString(event.stopReason) ?? readNonemptyString(readRecord(message)?.stopReason);
1247
+ const errorKind = readNonemptyString(event.errorKind);
1248
+ const base = {
1249
+ runId,
1250
+ ...message === void 0 ? {} : { message },
1251
+ scope
1252
+ };
1253
+ const next = reduceSessionProjection(projection, eventState === "delta" ? {
1254
+ type: "runDelta",
1255
+ ...base
1256
+ } : {
1257
+ type: "runTerminal",
1258
+ ...base,
1259
+ status: eventState === "aborted" ? "aborted" : eventState === "error" ? errorKind === "timeout" ? "timeout" : "error" : event.yielded === true && stopReason === "end_turn" ? "yielded" : stopReason === "error" ? "error" : "completed",
1260
+ ...stopReason === null ? {} : { stopReason },
1261
+ ...errorKind === null ? {} : { errorKind },
1262
+ ...typeof event.errorMessage === "string" ? { errorMessage: event.errorMessage } : {}
1263
+ });
1264
+ return {
1265
+ projection: next,
1266
+ previousRun: projection.runs[runId],
1267
+ currentRun: next.runs[runId]
1268
+ };
1269
+ }
1270
+ //#endregion
1271
+ //#region packages/gateway-client/src/session-subscriptions.ts
1272
+ function sessionSubscriptionParams(key, agentId) {
1273
+ return {
1274
+ key: key.trim(),
1275
+ ...agentId ? { agentId } : {}
1276
+ };
1277
+ }
1278
+ /**
1279
+ * One Gateway connection owns one targeted observer per canonical session.
1280
+ * Approval delivery is an upgrade of that observer, never a second observer.
1281
+ */
1282
+ var GatewaySessionMessageSubscriptionCoordinator = class {
1283
+ #client;
1284
+ #keysEquivalent;
1285
+ #entries = /* @__PURE__ */ new Set();
1286
+ #retired = false;
1287
+ constructor(client, options = {}) {
1288
+ this.#client = client;
1289
+ this.#keysEquivalent = options.keysEquivalent;
1290
+ }
1291
+ configure(options = {}) {
1292
+ const matcher = options.keysEquivalent;
1293
+ if (!matcher || matcher === this.#keysEquivalent) return this;
1294
+ if (this.#keysEquivalent || this.#entries.size > 0) throw new Error("Session message key equivalence cannot change for an active connection");
1295
+ this.#keysEquivalent = matcher;
1296
+ return this;
1297
+ }
1298
+ async acquire(key, options = {}) {
1299
+ const normalizedKey = key.trim();
1300
+ if (!normalizedKey) throw new Error("Session message subscription requires a session key");
1301
+ const agentId = options.agentId?.trim() || null;
1302
+ let entry;
1303
+ while (true) {
1304
+ if (this.#retired) throw new Error("Session message subscription belongs to a replaced Gateway connection");
1305
+ const existing = [...this.#entries].find((candidate) => candidate.agentId === agentId && (this.#areKeysEquivalent(candidate.key, normalizedKey) || [...candidate.requestedKeys].some((requestedKey) => this.#areKeysEquivalent(requestedKey, normalizedKey))));
1306
+ if (!existing) {
1307
+ const provisional = [...this.#entries].find((candidate) => candidate.agentId === agentId && !candidate.canonicalSettled);
1308
+ if (provisional) {
1309
+ await (provisional.plainFallback ?? provisional.ready).catch(() => void 0);
1310
+ continue;
1311
+ }
1312
+ entry = this.#createEntry(normalizedKey, agentId, options.includeApprovals === true);
1313
+ break;
1314
+ }
1315
+ if (!existing.release) {
1316
+ entry = existing;
1317
+ entry.requestedKeys.add(normalizedKey);
1318
+ break;
1319
+ }
1320
+ await existing.release.catch(() => void 0);
1321
+ }
1322
+ entry.pendingOwners += 1;
1323
+ try {
1324
+ const result = await this.#acquireCapability(entry, options.includeApprovals === true);
1325
+ if (this.#retired) throw new Error("Session message subscription completed on a replaced Gateway connection");
1326
+ const subscription = {
1327
+ key: result.key,
1328
+ agentId,
1329
+ ...options.includeApprovals === true ? {
1330
+ includeApprovals: true,
1331
+ ...result.approvalReplay !== void 0 ? { approvalReplay: result.approvalReplay } : {}
1332
+ } : {}
1333
+ };
1334
+ entry.handles.add(subscription);
1335
+ sessionMessageSubscriptionOwners.set(subscription, {
1336
+ coordinator: this,
1337
+ entry
1338
+ });
1339
+ return subscription;
1340
+ } finally {
1341
+ entry.pendingOwners -= 1;
1342
+ if (entry.pendingOwners === 0 && entry.handles.size === 0 && !entry.release) this.#entries.delete(entry);
1343
+ }
1344
+ }
1345
+ release(subscription) {
1346
+ const owner = sessionMessageSubscriptionOwners.get(subscription);
1347
+ if (!owner || owner.coordinator !== this) return Promise.resolve();
1348
+ const { entry } = owner;
1349
+ if (this.#retired || entry.handles.size > 1) {
1350
+ this.#finishRelease(subscription, owner);
1351
+ return Promise.resolve();
1352
+ }
1353
+ if (entry.release) return entry.release;
1354
+ if (entry.pendingOwners > 0) {
1355
+ const pending = [entry.ready, ...entry.approvalRequest ? [entry.approvalRequest] : []];
1356
+ const tracked = Promise.allSettled(pending).then(() => {
1357
+ if (entry.release === tracked) entry.release = null;
1358
+ return this.release(subscription);
1359
+ });
1360
+ entry.release = tracked;
1361
+ return tracked;
1362
+ }
1363
+ const tracked = this.#client.request("sessions.messages.unsubscribe", sessionSubscriptionParams(entry.key, entry.agentId)).then(() => {
1364
+ this.#finishRelease(subscription, owner, true);
1365
+ }).finally(() => {
1366
+ if (entry.release === tracked) entry.release = null;
1367
+ });
1368
+ entry.release = tracked;
1369
+ return tracked;
1370
+ }
1371
+ /** A reconnect retires leases without touching the next connection's observers. */
1372
+ reset() {
1373
+ this.#retired = true;
1374
+ for (const entry of this.#entries) for (const subscription of entry.handles) {
1375
+ const owner = sessionMessageSubscriptionOwners.get(subscription);
1376
+ if (owner?.coordinator === this) this.#finishRelease(subscription, owner);
1377
+ }
1378
+ this.#entries.clear();
1379
+ }
1380
+ #createEntry(key, agentId, includeApprovals) {
1381
+ const entry = {
1382
+ key,
1383
+ requestedKeys: /* @__PURE__ */ new Set([key]),
1384
+ agentId,
1385
+ ready: Promise.resolve({ key }),
1386
+ approvalRequest: null,
1387
+ approvalResponse: null,
1388
+ plainFallback: null,
1389
+ canonicalSettled: false,
1390
+ handles: /* @__PURE__ */ new Set(),
1391
+ pendingOwners: 0,
1392
+ release: null
1393
+ };
1394
+ entry.ready = this.#requestSubscribe(entry, includeApprovals).then((result) => {
1395
+ entry.key = result.key;
1396
+ entry.canonicalSettled = true;
1397
+ if (includeApprovals) entry.approvalResponse = result;
1398
+ return result;
1399
+ });
1400
+ if (includeApprovals) entry.approvalRequest = entry.ready;
1401
+ entry.ready.catch(() => void 0);
1402
+ this.#entries.add(entry);
1403
+ return entry;
1404
+ }
1405
+ #acquireCapability(entry, includeApprovals) {
1406
+ if (!includeApprovals) {
1407
+ if (entry.approvalRequest === entry.ready && !entry.approvalResponse) {
1408
+ if (!entry.plainFallback) {
1409
+ const approvalRequest = entry.ready;
1410
+ entry.plainFallback = approvalRequest.catch(async (error) => {
1411
+ if (this.#retired) throw error;
1412
+ const result = await this.#requestSubscribe(entry, false);
1413
+ entry.key = result.key;
1414
+ entry.canonicalSettled = true;
1415
+ entry.ready = Promise.resolve(result);
1416
+ if (entry.approvalRequest === approvalRequest) entry.approvalRequest = null;
1417
+ return result;
1418
+ });
1419
+ }
1420
+ return entry.plainFallback;
1421
+ }
1422
+ return entry.ready;
1423
+ }
1424
+ if (entry.approvalResponse) return Promise.resolve(entry.approvalResponse);
1425
+ if (entry.approvalRequest) return entry.approvalRequest;
1426
+ const upgrade = entry.ready.then(() => this.#requestSubscribe(entry, true)).then((result) => {
1427
+ entry.key = result.key;
1428
+ entry.approvalResponse = result;
1429
+ return result;
1430
+ });
1431
+ entry.approvalRequest = upgrade;
1432
+ upgrade.catch(() => {
1433
+ if (entry.approvalRequest === upgrade) entry.approvalRequest = null;
1434
+ });
1435
+ return upgrade;
1436
+ }
1437
+ async #requestSubscribe(entry, includeApprovals) {
1438
+ const result = await this.#client.request("sessions.messages.subscribe", {
1439
+ ...sessionSubscriptionParams(entry.key, entry.agentId),
1440
+ ...includeApprovals ? { includeApprovals: true } : {}
1441
+ });
1442
+ const response = result && typeof result === "object" ? result : null;
1443
+ const responseKey = response && "key" in response ? response.key : void 0;
1444
+ return {
1445
+ key: typeof responseKey === "string" && responseKey.trim() ? responseKey.trim() : entry.key,
1446
+ ...response && "approvalReplay" in response ? { approvalReplay: response.approvalReplay } : {}
1447
+ };
1448
+ }
1449
+ #finishRelease(subscription, owner, removeEntry = false) {
1450
+ if (sessionMessageSubscriptionOwners.get(subscription) !== owner) return;
1451
+ sessionMessageSubscriptionOwners.delete(subscription);
1452
+ owner.entry.handles.delete(subscription);
1453
+ if (removeEntry) this.#entries.delete(owner.entry);
1454
+ }
1455
+ #areKeysEquivalent(left, right) {
1456
+ return left === right || this.#keysEquivalent?.(left, right) === true;
1457
+ }
1458
+ };
1459
+ const sessionMessageSubscriptionOwners = /* @__PURE__ */ new WeakMap();
1460
+ const sessionMessageSubscriptionCoordinators = /* @__PURE__ */ new WeakMap();
1461
+ function getGatewaySessionMessageSubscriptionCoordinator(client, options = {}) {
1462
+ const existing = sessionMessageSubscriptionCoordinators.get(client);
1463
+ if (existing) return existing.configure(options);
1464
+ const coordinator = new GatewaySessionMessageSubscriptionCoordinator(client, options);
1465
+ sessionMessageSubscriptionCoordinators.set(client, coordinator);
1466
+ return coordinator;
1467
+ }
1468
+ function resetGatewaySessionMessageSubscriptionCoordinator(client) {
1469
+ sessionMessageSubscriptionCoordinators.get(client)?.reset();
1470
+ sessionMessageSubscriptionCoordinators.delete(client);
1471
+ }
1472
+ function releaseGatewaySessionMessageSubscription(subscription) {
1473
+ return sessionMessageSubscriptionOwners.get(subscription)?.coordinator.release(subscription) ?? Promise.resolve();
1474
+ }
1475
+ //#endregion
1476
+ export { buildDeviceAuthPayload as C, shouldRetryGatewayWithDeviceToken as S, normalizeDeviceMetadataForAuth as T, GatewayProtocolRequestError as _, createSessionProjection as a, resolveGatewayConnectScopes as b, normalizeSessionProjectionRunId as c, readSessionMessageSequence as d, reconcileSessionProjectionSnapshot as f, GatewayProtocolClient as g, shouldPauseGatewayReconnect as h, resetGatewaySessionMessageSubscriptionCoordinator as i, projectLiveSessionMessage as l, reduceSessionProjectionRunEvent as m, getGatewaySessionMessageSubscriptionCoordinator as n, hasSessionProjectionAcceptedFinal as o, reduceSessionProjection as p, releaseGatewaySessionMessageSubscription as r, isLocallyOptimisticSessionMessage as s, GatewaySessionMessageSubscriptionCoordinator as t, readSessionMessageIdentity as u, GatewayBrowserDeviceAuthLifecycle as v, buildDeviceAuthPayloadV3 as w, selectGatewayConnectAuth as x, buildGatewayConnectAuth as y };