@openclaw/gateway-client 2026.9.5 → 2026.9.7
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.
- package/README.md +78 -3
- package/dist/browser.d.mts +24 -4
- package/dist/browser.mjs +51 -2
- package/dist/index.d.mts +22 -4
- package/dist/index.mjs +124 -77
- package/dist/{protocol-client-DZ7F1DIs.d.mts → protocol-client-DT451Msj.d.mts} +19 -2
- package/dist/{protocol-request-B5gBmLgq.d.mts → protocol-request-CtVhe3Bj.d.mts} +1 -1
- package/dist/{readiness-B99dV5nY.d.mts → readiness-Cj5C24o2.d.mts} +16 -24
- package/dist/readiness.d.mts +1 -1
- package/dist/scope-upgrade.d.mts +2 -1
- package/dist/scope-upgrade.mjs +12 -2
- package/dist/{session-subscriptions-yRMtCvZZ.d.mts → session-subscriptions-Bcq6_wtR.d.mts} +18 -4
- package/dist/{session-subscriptions-DrOaOXNR.mjs → session-subscriptions-LtlX9oRQ.mjs} +359 -123
- package/dist/timeouts.mjs +1 -1
- package/package.json +3 -3
|
@@ -1,13 +1,12 @@
|
|
|
1
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
|
+
import { GATEWAY_SUSPEND_IDENTITY_RETRY_AFTER_MS, isGatewaySuspendUnavailableError } from "@openclaw/gateway-protocol/restart-unavailable";
|
|
4
5
|
import { GATEWAY_ASSISTANT_ERROR_FALLBACK_TEXT } from "@openclaw/gateway-protocol/gateway-error-details";
|
|
5
6
|
//#region packages/gateway-client/src/device-auth.ts
|
|
6
7
|
function normalizeDeviceMetadataForAuth(value) {
|
|
7
8
|
if (typeof value !== "string") return "";
|
|
8
|
-
|
|
9
|
-
if (!trimmed) return "";
|
|
10
|
-
return trimmed.replace(/[A-Z]/g, (char) => String.fromCharCode(char.charCodeAt(0) + 32));
|
|
9
|
+
return value.trim().replace(/[A-Z]/g, (char) => String.fromCharCode(char.charCodeAt(0) + 32));
|
|
11
10
|
}
|
|
12
11
|
function buildDeviceAuthPayload(params) {
|
|
13
12
|
const scopes = params.scopes.join(",");
|
|
@@ -44,6 +43,63 @@ function buildDeviceAuthPayloadV3(params) {
|
|
|
44
43
|
].join("|");
|
|
45
44
|
}
|
|
46
45
|
//#endregion
|
|
46
|
+
//#region packages/normalization-core/src/record-coerce.ts
|
|
47
|
+
/** Type guard for non-array object records at browser-safe boundaries. */
|
|
48
|
+
function isRecord(value) {
|
|
49
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
50
|
+
}
|
|
51
|
+
/** Coerces object-like values to records, falling back to an empty record. */
|
|
52
|
+
function asRecord(value) {
|
|
53
|
+
return asOptionalObjectRecord(value) ?? {};
|
|
54
|
+
}
|
|
55
|
+
/** Returns a non-array record or null. */
|
|
56
|
+
function asNullableRecord(value) {
|
|
57
|
+
return isRecord(value) ? value : null;
|
|
58
|
+
}
|
|
59
|
+
/** Returns any object-backed record, including arrays, or undefined. */
|
|
60
|
+
function asOptionalObjectRecord(value) {
|
|
61
|
+
return value && typeof value === "object" ? value : void 0;
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region packages/gateway-client/src/chat-stream-message.ts
|
|
65
|
+
/** A snapshot includes its delta; append-only frames require an existing baseline. */
|
|
66
|
+
function mergeChatStreamMessage(previous, payload) {
|
|
67
|
+
if (payload.message !== void 0) return payload.message;
|
|
68
|
+
const delta = payload.deltaText;
|
|
69
|
+
if (typeof delta !== "string") return previous;
|
|
70
|
+
const replace = payload.replace === true;
|
|
71
|
+
if (!delta && !replace) return previous;
|
|
72
|
+
if (!isRecord(previous) && !replace) return;
|
|
73
|
+
const message = isRecord(previous) ? previous : { role: "assistant" };
|
|
74
|
+
if (typeof message.content === "string") return {
|
|
75
|
+
...message,
|
|
76
|
+
content: replace ? delta : message.content + delta
|
|
77
|
+
};
|
|
78
|
+
const content = Array.isArray(message.content) ? [...message.content] : [];
|
|
79
|
+
const isText = (block) => isRecord(block) && block.type === "text";
|
|
80
|
+
if (replace) return {
|
|
81
|
+
...message,
|
|
82
|
+
content: [{
|
|
83
|
+
type: "text",
|
|
84
|
+
text: delta
|
|
85
|
+
}, ...content.filter((block) => !isText(block))]
|
|
86
|
+
};
|
|
87
|
+
const index = content.findLastIndex(isText);
|
|
88
|
+
const block = content[index];
|
|
89
|
+
if (isText(block)) content[index] = {
|
|
90
|
+
...block,
|
|
91
|
+
text: `${typeof block.text === "string" ? block.text : ""}${delta}`
|
|
92
|
+
};
|
|
93
|
+
else content.push({
|
|
94
|
+
type: "text",
|
|
95
|
+
text: delta
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
...message,
|
|
99
|
+
content
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
47
103
|
//#region packages/gateway-client/src/connect-auth.ts
|
|
48
104
|
function normalized(value) {
|
|
49
105
|
return typeof value === "string" ? value.trim() || void 0 : void 0;
|
|
@@ -114,21 +170,19 @@ var GatewayBrowserDeviceAuthLifecycle = class {
|
|
|
114
170
|
deviceId: identity.deviceId,
|
|
115
171
|
role: params.role
|
|
116
172
|
}) : null;
|
|
117
|
-
const storedValue = stored?.token;
|
|
118
173
|
const selectedAuth = selectGatewayConnectAuth({
|
|
119
174
|
token: params.token,
|
|
120
175
|
bootstrapToken: params.bootstrapToken,
|
|
121
176
|
password: params.password,
|
|
122
|
-
storedToken:
|
|
177
|
+
storedToken: stored?.token,
|
|
123
178
|
storedScopes: stored?.scopes,
|
|
124
179
|
pendingDeviceTokenRetry: params.pendingDeviceTokenRetry,
|
|
125
180
|
trustedDeviceTokenRetry: params.trustedDeviceTokenRetry,
|
|
126
181
|
preferBootstrapToken: params.preferBootstrapToken
|
|
127
182
|
});
|
|
128
|
-
const { usingStoredDeviceToken } = selectedAuth;
|
|
129
183
|
const scopes = resolveGatewayConnectScopes({
|
|
130
184
|
requestedScopes: selectedAuth.authBootstrapToken ? params.bootstrapScopes ? [...params.bootstrapScopes] : void 0 : void 0,
|
|
131
|
-
usingStoredDeviceToken,
|
|
185
|
+
usingStoredDeviceToken: selectedAuth.usingStoredDeviceToken,
|
|
132
186
|
storedScopes: selectedAuth.storedScopes,
|
|
133
187
|
defaultScopes: params.defaultScopes
|
|
134
188
|
});
|
|
@@ -143,10 +197,6 @@ var GatewayBrowserDeviceAuthLifecycle = class {
|
|
|
143
197
|
const signedAtMs = params.challengeTs === void 0 ? this.deps.nowMs?.() ?? Date.now() : params.challengeTs;
|
|
144
198
|
if (typeof signedAtMs !== "number" || !Number.isSafeInteger(signedAtMs) || signedAtMs < 0) throw new Error("gateway connect challenge timestamp invalid");
|
|
145
199
|
const nonce = params.nonce ?? "";
|
|
146
|
-
const { authBootstrapToken: primary, signatureToken: signed } = selectedAuth;
|
|
147
|
-
let token = null;
|
|
148
|
-
if (primary) token = primary;
|
|
149
|
-
else if (signed) token = signed;
|
|
150
200
|
const payload = buildDeviceAuthPayloadV3({
|
|
151
201
|
deviceId: identity.deviceId,
|
|
152
202
|
clientId: params.client.id,
|
|
@@ -154,7 +204,7 @@ var GatewayBrowserDeviceAuthLifecycle = class {
|
|
|
154
204
|
role: params.role,
|
|
155
205
|
scopes,
|
|
156
206
|
signedAtMs,
|
|
157
|
-
token,
|
|
207
|
+
token: selectedAuth.authBootstrapToken || selectedAuth.signatureToken || null,
|
|
158
208
|
nonce,
|
|
159
209
|
platform: params.client.platform,
|
|
160
210
|
deviceFamily: params.client.deviceFamily
|
|
@@ -245,9 +295,12 @@ function computeBackoff(policy, attempt) {
|
|
|
245
295
|
const jitter = base * policy.jitter * Math.random();
|
|
246
296
|
return Math.min(policy.maxMs, Math.round(base + jitter));
|
|
247
297
|
}
|
|
298
|
+
function resolveSleepDelayMs(ms) {
|
|
299
|
+
return Number.isFinite(ms) && ms > 0 ? Math.min(Math.max(Math.floor(ms), 1), MAX_TIMER_TIMEOUT_MS) : 0;
|
|
300
|
+
}
|
|
248
301
|
async function sleepWithAbort(ms, abortSignal, options = {}) {
|
|
249
|
-
|
|
250
|
-
|
|
302
|
+
const delayMs = resolveSleepDelayMs(ms);
|
|
303
|
+
if (delayMs === 0) return;
|
|
251
304
|
await new Promise((resolve, reject) => {
|
|
252
305
|
let settled = false;
|
|
253
306
|
let timer = null;
|
|
@@ -330,21 +383,18 @@ const defaultSleep = async (ms) => {
|
|
|
330
383
|
} while (remainingMs > 0);
|
|
331
384
|
};
|
|
332
385
|
function clampNumber(value, fallback, min, max) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
return Math.min(Math.max(next, min ?? Number.NEGATIVE_INFINITY), max ?? Number.POSITIVE_INFINITY);
|
|
386
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
387
|
+
return Math.min(Math.max(value, min ?? Number.NEGATIVE_INFINITY), max ?? Number.POSITIVE_INFINITY);
|
|
336
388
|
}
|
|
337
389
|
function resolveAttemptCount(value, fallback) {
|
|
338
|
-
return Math.max(1, Math.round(
|
|
390
|
+
return Math.max(1, Math.round(clampNumber(value, fallback)));
|
|
339
391
|
}
|
|
340
392
|
function resolveRetryDelayMs(value) {
|
|
341
|
-
|
|
342
|
-
return Math.min(Math.max(Math.round(finite), 0), MAX_TIMER_TIMEOUT_MS);
|
|
393
|
+
return Math.min(Math.max(Math.round(value === Number.POSITIVE_INFINITY ? MAX_TIMER_TIMEOUT_MS : Number.isFinite(value) ? value : 0), 0), MAX_TIMER_TIMEOUT_MS);
|
|
343
394
|
}
|
|
344
395
|
function resolveJitterConfig(value, fallback) {
|
|
345
396
|
if (value === "full") return "full";
|
|
346
|
-
|
|
347
|
-
return fraction === void 0 ? fallback : Math.min(Math.max(fraction, 0), 1);
|
|
397
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.min(Math.max(value, 0), 1) : fallback;
|
|
348
398
|
}
|
|
349
399
|
function resolveRetryConfig(defaults = DEFAULT_RETRY_CONFIG, overrides) {
|
|
350
400
|
const attempts = resolveAttemptCount(overrides?.attempts, defaults.attempts);
|
|
@@ -499,6 +549,20 @@ var GatewayPendingRequests = class {
|
|
|
499
549
|
this.pending = /* @__PURE__ */ new Map();
|
|
500
550
|
this.requestSequence = 0;
|
|
501
551
|
}
|
|
552
|
+
setSuspensionPhase(phase) {
|
|
553
|
+
if (phase === "accepting") this.resumeIdentityRequests();
|
|
554
|
+
else if (phase === "preparing" || phase === "draining" || phase === "prepared") this.pauseIdentityRequests();
|
|
555
|
+
}
|
|
556
|
+
pauseIdentityRequests(retryAfterMs = GATEWAY_SUSPEND_IDENTITY_RETRY_AFTER_MS) {
|
|
557
|
+
clearTimeout(this.suspensionTimer);
|
|
558
|
+
this.suspensionTimer = setTimeout(() => this.resumeIdentityRequests(), resolveSafeTimeoutDelayMs(Number.isFinite(retryAfterMs) && retryAfterMs > 0 ? retryAfterMs : GATEWAY_SUSPEND_IDENTITY_RETRY_AFTER_MS));
|
|
559
|
+
this.suspensionTimer.unref?.();
|
|
560
|
+
}
|
|
561
|
+
resumeIdentityRequests() {
|
|
562
|
+
clearTimeout(this.suspensionTimer);
|
|
563
|
+
this.suspensionTimer = void 0;
|
|
564
|
+
for (const pending of this.pending.values()) if (pending.waitingForResume) pending.resend?.();
|
|
565
|
+
}
|
|
502
566
|
get hasPending() {
|
|
503
567
|
return this.pending.size > 0;
|
|
504
568
|
}
|
|
@@ -562,15 +626,27 @@ var GatewayPendingRequests = class {
|
|
|
562
626
|
options?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
563
627
|
this.pending.set(id, pending);
|
|
564
628
|
try {
|
|
565
|
-
|
|
629
|
+
const frame = JSON.stringify({
|
|
566
630
|
type: "req",
|
|
567
631
|
id,
|
|
568
632
|
method,
|
|
569
633
|
params
|
|
570
|
-
})
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
634
|
+
});
|
|
635
|
+
const send = () => {
|
|
636
|
+
if (this.pending.get(id) !== pending) return;
|
|
637
|
+
pending.waitingForResume = Boolean(pending.resend && this.suspensionTimer);
|
|
638
|
+
if (pending.waitingForResume) return;
|
|
639
|
+
try {
|
|
640
|
+
sender.send(frame);
|
|
641
|
+
if (this.pending.get(id) !== pending) return;
|
|
642
|
+
requestSent = true;
|
|
643
|
+
this.invoke("sent", () => options?.onSent?.(id));
|
|
644
|
+
} catch (error) {
|
|
645
|
+
if (retire("CLIENT_SEND_ERROR")) reject(error instanceof Error ? error : new Error(String(error)));
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
if (method === "agent.identity.get") pending.resend = send;
|
|
649
|
+
send();
|
|
574
650
|
} catch (error) {
|
|
575
651
|
if (retire("CLIENT_SEND_ERROR")) reject(error instanceof Error ? error : new Error(String(error)));
|
|
576
652
|
}
|
|
@@ -587,6 +663,11 @@ var GatewayPendingRequests = class {
|
|
|
587
663
|
}
|
|
588
664
|
return;
|
|
589
665
|
}
|
|
666
|
+
if (!frame.ok && pending.resend && !pending.acceptedNotified && frame.error?.code === "UNAVAILABLE" && frame.error.retryable === true && isGatewaySuspendUnavailableError(frame.error)) {
|
|
667
|
+
this.pauseIdentityRequests(frame.error.retryAfterMs);
|
|
668
|
+
pending.waitingForResume = true;
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
590
671
|
this.pending.delete(frame.id);
|
|
591
672
|
pending.cleanup?.();
|
|
592
673
|
if (frame.ok) {
|
|
@@ -600,6 +681,8 @@ var GatewayPendingRequests = class {
|
|
|
600
681
|
pending.reject(error);
|
|
601
682
|
}
|
|
602
683
|
flush(error) {
|
|
684
|
+
clearTimeout(this.suspensionTimer);
|
|
685
|
+
this.suspensionTimer = void 0;
|
|
603
686
|
const retired = this.pending;
|
|
604
687
|
this.pending = /* @__PURE__ */ new Map();
|
|
605
688
|
this.requestSequence = 0;
|
|
@@ -615,19 +698,15 @@ var GatewayPendingRequests = class {
|
|
|
615
698
|
}
|
|
616
699
|
finishTiming(id, pending, ok, errorCode) {
|
|
617
700
|
const endedAtMs = this.opts.nowMs();
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
});
|
|
628
|
-
} catch (error) {
|
|
629
|
-
this.opts.onCallbackError?.("request timing", error);
|
|
630
|
-
}
|
|
701
|
+
this.invoke("request timing", () => this.opts.onTiming?.({
|
|
702
|
+
id,
|
|
703
|
+
method: pending.method,
|
|
704
|
+
ok,
|
|
705
|
+
durationMs: Math.max(0, endedAtMs - pending.startedAtMs),
|
|
706
|
+
startedAtMs: pending.startedAtMs,
|
|
707
|
+
endedAtMs,
|
|
708
|
+
errorCode
|
|
709
|
+
}));
|
|
631
710
|
}
|
|
632
711
|
invoke(label, callback) {
|
|
633
712
|
try {
|
|
@@ -872,6 +951,7 @@ var GatewayProtocolClient = class {
|
|
|
872
951
|
this.request("connect", this.opts.buildConnectParams(plan)).then((hello) => {
|
|
873
952
|
if (!this.isConnectCurrent(socket, generation)) return;
|
|
874
953
|
this.helloReceived = true;
|
|
954
|
+
this.requests.setSuspensionPhase(hello.snapshot?.suspension?.phase);
|
|
875
955
|
this.clearHandshakeTimer();
|
|
876
956
|
this.connectFailure = void 0;
|
|
877
957
|
this.reconnectSupervisor.reset();
|
|
@@ -910,7 +990,7 @@ var GatewayProtocolClient = class {
|
|
|
910
990
|
});
|
|
911
991
|
}
|
|
912
992
|
handleMessage(socket, generation, raw) {
|
|
913
|
-
if (!this.isActive(socket, generation)) return;
|
|
993
|
+
if (!this.isActive(socket, generation) || this.connectionAbort?.signal.aborted) return;
|
|
914
994
|
let parsed;
|
|
915
995
|
try {
|
|
916
996
|
parsed = JSON.parse(raw);
|
|
@@ -943,26 +1023,34 @@ var GatewayProtocolClient = class {
|
|
|
943
1023
|
if (seq !== null) {
|
|
944
1024
|
if (this.lastSeq !== null && seq > this.lastSeq + 1) {
|
|
945
1025
|
const expected = this.lastSeq + 1;
|
|
1026
|
+
const state = asRecord(parsed.payload).state;
|
|
1027
|
+
if (parsed.event === "chat" && (state === "final" || state === "error" || state === "aborted")) this.dispatchEvent(socket, generation, parsed);
|
|
1028
|
+
if (!this.isActive(socket, generation) || this.connectionAbort?.signal.aborted) return;
|
|
946
1029
|
this.invoke("gap", () => this.opts.onGap?.({
|
|
947
1030
|
expected,
|
|
948
1031
|
received: seq
|
|
949
1032
|
}));
|
|
950
|
-
if (
|
|
1033
|
+
if (this.isActive(socket, generation) && !this.connectionAbort?.signal.aborted) this.closeSocket(4e3, "event sequence gap");
|
|
1034
|
+
return;
|
|
951
1035
|
}
|
|
952
1036
|
this.lastSeq = seq;
|
|
953
1037
|
}
|
|
954
|
-
|
|
955
|
-
this.invoke("event", () => this.opts.onEvent?.(parsed));
|
|
956
|
-
for (const [listener, subscription] of listeners) {
|
|
957
|
-
if (!this.isActive(socket, generation)) return;
|
|
958
|
-
if (this.listeners.isCurrent(listener, subscription)) this.invoke("event listener", () => listener(parsed));
|
|
959
|
-
}
|
|
1038
|
+
this.dispatchEvent(socket, generation, parsed);
|
|
960
1039
|
return;
|
|
961
1040
|
}
|
|
962
1041
|
if (!isGatewayResponseFrame(parsed)) return;
|
|
963
1042
|
this.opts.onActivity?.();
|
|
964
1043
|
this.requests.handleResponse(parsed);
|
|
965
1044
|
}
|
|
1045
|
+
dispatchEvent(socket, generation, event) {
|
|
1046
|
+
const listeners = this.listeners.snapshot();
|
|
1047
|
+
if (event.event === "gateway.suspension" && typeof event.payload === "object" && event.payload !== null && "phase" in event.payload) this.requests.setSuspensionPhase(event.payload.phase);
|
|
1048
|
+
this.invoke("event", () => this.opts.onEvent?.(event));
|
|
1049
|
+
for (const [listener, subscription] of listeners) {
|
|
1050
|
+
if (!this.isActive(socket, generation) || this.connectionAbort?.signal.aborted) return;
|
|
1051
|
+
if (this.listeners.isCurrent(listener, subscription)) this.invoke("event listener", () => listener(event));
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
966
1054
|
handleClose(socket, generation, code, reason) {
|
|
967
1055
|
if (this.socket !== socket) {
|
|
968
1056
|
if (this.stoppedSocket?.socket === socket) {
|
|
@@ -1008,13 +1096,23 @@ var GatewayProtocolClient = class {
|
|
|
1008
1096
|
const retry = this.reconnectSupervisor.next();
|
|
1009
1097
|
if (!retry) return;
|
|
1010
1098
|
this.reconnectSignal = retry.signal;
|
|
1011
|
-
|
|
1099
|
+
let delayMs = overrideMs;
|
|
1100
|
+
if (delayMs === void 0) {
|
|
1101
|
+
const base = Math.max(retry.delayMs, minimumMs);
|
|
1102
|
+
const ceiling = Math.max(this.opts.reconnect.maxMs, Math.min(Number.MAX_VALUE, minimumMs * 1.2));
|
|
1103
|
+
const lower = Math.max(minimumMs, Math.min(base, ceiling / 1.2));
|
|
1104
|
+
const upper = Math.min(base * 1.2, ceiling);
|
|
1105
|
+
delayMs = Math.ceil(lower + Math.random() * (upper - lower));
|
|
1106
|
+
}
|
|
1107
|
+
delayMs = resolveSleepDelayMs(delayMs);
|
|
1108
|
+
sleepWithAbort(delayMs, retry.signal).then(() => {
|
|
1012
1109
|
if (this.reconnectSignal !== retry.signal) return;
|
|
1013
1110
|
this.reconnectSignal = null;
|
|
1014
1111
|
this.invoke("reconnect", () => this.connect());
|
|
1015
1112
|
}, () => {
|
|
1016
1113
|
if (this.reconnectSignal === retry.signal) this.reconnectSignal = null;
|
|
1017
1114
|
});
|
|
1115
|
+
this.invoke("reconnect scheduled", () => this.opts.onReconnectScheduled?.(delayMs, retry.signal));
|
|
1018
1116
|
}
|
|
1019
1117
|
closeContext() {
|
|
1020
1118
|
return {
|
|
@@ -1083,23 +1181,13 @@ function shouldPauseGatewayReconnect(params) {
|
|
|
1083
1181
|
return NON_RECOVERABLE_AUTH_ERRORS.has(code) || params.protocolMismatchIsTerminal === true && code === ConnectErrorDetailCodes.PROTOCOL_MISMATCH || params.clientVersionMismatchIsTerminal === true && code === ConnectErrorDetailCodes.CLIENT_VERSION_MISMATCH;
|
|
1084
1182
|
}
|
|
1085
1183
|
//#endregion
|
|
1086
|
-
//#region packages/normalization-core/src/record-coerce.ts
|
|
1087
|
-
/** Type guard for non-array object records at browser-safe boundaries. */
|
|
1088
|
-
function isRecord(value) {
|
|
1089
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1090
|
-
}
|
|
1091
|
-
/** Returns a non-array record or null. */
|
|
1092
|
-
function asNullableRecord(value) {
|
|
1093
|
-
return isRecord(value) ? value : null;
|
|
1094
|
-
}
|
|
1095
|
-
//#endregion
|
|
1096
1184
|
//#region packages/normalization-core/src/stable-stringify.ts
|
|
1097
1185
|
const preserveString = (value) => value;
|
|
1098
1186
|
/** Deterministically stringifies values, optionally normalizing strings before key ordering. */
|
|
1099
1187
|
function stableStringify(value, normalizeString = preserveString) {
|
|
1100
1188
|
return stringifyStableValue(value, /* @__PURE__ */ new WeakSet(), normalizeString);
|
|
1101
1189
|
}
|
|
1102
|
-
function stringifyStableValue(value, stack, normalizeString) {
|
|
1190
|
+
function stringifyStableValue(value, stack, normalizeString, write) {
|
|
1103
1191
|
if (value === null || value === void 0) return String(value);
|
|
1104
1192
|
if (typeof value === "number" && !Number.isFinite(value)) return JSON.stringify(String(value));
|
|
1105
1193
|
if (typeof value === "bigint") return JSON.stringify(value.toString());
|
|
@@ -1108,22 +1196,33 @@ function stringifyStableValue(value, stack, normalizeString) {
|
|
|
1108
1196
|
if (stack.has(value)) return JSON.stringify("[Circular]");
|
|
1109
1197
|
stack.add(value);
|
|
1110
1198
|
try {
|
|
1111
|
-
return stringifyObjectValue(value, stack, normalizeString);
|
|
1199
|
+
return stringifyObjectValue(value, stack, normalizeString, write);
|
|
1112
1200
|
} finally {
|
|
1113
1201
|
stack.delete(value);
|
|
1114
1202
|
}
|
|
1115
1203
|
}
|
|
1116
|
-
function stringifyObjectValue(value, stack, normalizeString) {
|
|
1204
|
+
function stringifyObjectValue(value, stack, normalizeString, write) {
|
|
1117
1205
|
if (value instanceof Error) return stringifyStableValue({
|
|
1118
1206
|
name: value.name,
|
|
1119
1207
|
message: value.message,
|
|
1120
1208
|
stack: value.stack
|
|
1121
|
-
}, stack, normalizeString);
|
|
1209
|
+
}, stack, normalizeString, write);
|
|
1122
1210
|
if (value instanceof Uint8Array) return stringifyStableValue({
|
|
1123
1211
|
type: "Uint8Array",
|
|
1124
1212
|
data: encodeBase64(value)
|
|
1125
|
-
}, stack, normalizeString);
|
|
1213
|
+
}, stack, normalizeString, write);
|
|
1126
1214
|
if (Array.isArray(value)) {
|
|
1215
|
+
if (write) {
|
|
1216
|
+
write("[");
|
|
1217
|
+
let separator = "";
|
|
1218
|
+
for (const entry of value) {
|
|
1219
|
+
write(separator);
|
|
1220
|
+
write(stringifyStableValue(entry, stack, normalizeString, write));
|
|
1221
|
+
separator = ",";
|
|
1222
|
+
}
|
|
1223
|
+
write("]");
|
|
1224
|
+
return "";
|
|
1225
|
+
}
|
|
1127
1226
|
const serializedEntries = [];
|
|
1128
1227
|
for (const entry of value) serializedEntries.push(stringifyStableValue(entry, stack, normalizeString));
|
|
1129
1228
|
return `[${serializedEntries.join(",")}]`;
|
|
@@ -1131,6 +1230,17 @@ function stringifyObjectValue(value, stack, normalizeString) {
|
|
|
1131
1230
|
const record = value;
|
|
1132
1231
|
if (normalizeString === preserveString) {
|
|
1133
1232
|
const fields = Object.keys(record).sort();
|
|
1233
|
+
if (write) {
|
|
1234
|
+
write("{");
|
|
1235
|
+
let separator = "";
|
|
1236
|
+
for (const key of fields) {
|
|
1237
|
+
write(`${separator}${JSON.stringify(key)}:`);
|
|
1238
|
+
write(stringifyStableValue(record[key], stack, normalizeString, write));
|
|
1239
|
+
separator = ",";
|
|
1240
|
+
}
|
|
1241
|
+
write("}");
|
|
1242
|
+
return "";
|
|
1243
|
+
}
|
|
1134
1244
|
let fieldIndex = 0;
|
|
1135
1245
|
for (const key of fields) fields[fieldIndex++] = `${JSON.stringify(key)}:${stringifyStableValue(record[key], stack, normalizeString)}`;
|
|
1136
1246
|
return `{${fields.join(",")}}`;
|
|
@@ -1142,6 +1252,17 @@ function stringifyObjectValue(value, stack, normalizeString) {
|
|
|
1142
1252
|
return compareStableStrings(left.normalizedKey, right.normalizedKey) || compareStableStrings(left.key, right.key);
|
|
1143
1253
|
});
|
|
1144
1254
|
const serializedFields = [];
|
|
1255
|
+
if (write) {
|
|
1256
|
+
write("{");
|
|
1257
|
+
let separator = "";
|
|
1258
|
+
for (const { key, normalizedKey } of entries) {
|
|
1259
|
+
write(`${separator}${JSON.stringify(normalizedKey)}:`);
|
|
1260
|
+
write(stringifyStableValue(record[key], stack, normalizeString, write));
|
|
1261
|
+
separator = ",";
|
|
1262
|
+
}
|
|
1263
|
+
write("}");
|
|
1264
|
+
return "";
|
|
1265
|
+
}
|
|
1145
1266
|
for (const { key, normalizedKey } of entries) serializedFields.push(`${JSON.stringify(normalizedKey)}:${stringifyStableValue(record[key], stack, normalizeString)}`);
|
|
1146
1267
|
return `{${serializedFields.join(",")}}`;
|
|
1147
1268
|
}
|
|
@@ -1282,6 +1403,18 @@ function readSessionMessageDisplayContent(message) {
|
|
|
1282
1403
|
usesFallbackText: fallback !== null
|
|
1283
1404
|
};
|
|
1284
1405
|
}
|
|
1406
|
+
/** Status notices remain visible but do not define the terminal reply they accompany. */
|
|
1407
|
+
function projectSessionTerminalReplyMessage(message) {
|
|
1408
|
+
const record = asNullableRecord(message);
|
|
1409
|
+
if (!record || !Array.isArray(record.content)) return message;
|
|
1410
|
+
const content = record.content.filter((block) => asNullableRecord(block)?.openclawStatusNotice !== true);
|
|
1411
|
+
if (content.length === record.content.length || content.length === 0) return message;
|
|
1412
|
+
return {
|
|
1413
|
+
...record,
|
|
1414
|
+
content,
|
|
1415
|
+
text: void 0
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1285
1418
|
/** Check whether a projected message has text or another displayable block. */
|
|
1286
1419
|
function hasDisplayableSessionMessage(message) {
|
|
1287
1420
|
const { text, hasNonText } = readSessionMessageDisplayContent(message);
|
|
@@ -1301,12 +1434,7 @@ function isSessionProjectionErrorMessage(message, errorMessage) {
|
|
|
1301
1434
|
//#endregion
|
|
1302
1435
|
//#region packages/gateway-client/src/session-projection-final-identity.ts
|
|
1303
1436
|
/** Terminal identity rules used to reconcile live and durable assistant projections. */
|
|
1304
|
-
/**
|
|
1305
|
-
* The class of rows the #148297 relaxation newly admits as finals: persisted
|
|
1306
|
-
* with the run's terminal tool stop reason while carrying no tool-call
|
|
1307
|
-
* content. Both adoption directions and every position rule scope to this
|
|
1308
|
-
* predicate, so pre-existing match semantics stay untouched.
|
|
1309
|
-
*/
|
|
1437
|
+
/** A run's selected text reply can inherit its terminal tool stop reason. */
|
|
1310
1438
|
function isToolUsePersistedFinalRow(message) {
|
|
1311
1439
|
return asNullableRecord(message)?.["stopReason"] === "toolUse" && !isSessionProjectionToolContinuation(message);
|
|
1312
1440
|
}
|
|
@@ -1341,10 +1469,11 @@ function hasCompatiblePersistedFinalIdentity(currentMessage, incomingMessage) {
|
|
|
1341
1469
|
return current.sequence !== null && incoming.sequence !== null && current.sequence === incoming.sequence;
|
|
1342
1470
|
}
|
|
1343
1471
|
function readFinalContentIdentity(message) {
|
|
1344
|
-
const
|
|
1472
|
+
const terminalMessage = projectSessionTerminalReplyMessage(message);
|
|
1473
|
+
const display = readSessionMessageDisplayContent(terminalMessage);
|
|
1345
1474
|
if (!display.text && !display.hasNonText) return null;
|
|
1346
1475
|
const identity = readSessionMessageIdentity(message);
|
|
1347
|
-
const record = asNullableRecord(
|
|
1476
|
+
const record = asNullableRecord(terminalMessage);
|
|
1348
1477
|
const metadata = asNullableRecord(record?.["__openclaw"]);
|
|
1349
1478
|
try {
|
|
1350
1479
|
return `content:${stableStringify([
|
|
@@ -1402,22 +1531,15 @@ function findUniqueSnapshotTerminalMatch(current, matches, run, snapshot) {
|
|
|
1402
1531
|
});
|
|
1403
1532
|
snapshotIndexes = indexes;
|
|
1404
1533
|
};
|
|
1405
|
-
const
|
|
1406
|
-
const runId = current.identity?.runId;
|
|
1407
|
-
if (!runId || entry.identity?.runId !== runId) return false;
|
|
1408
|
-
ensureRunIndexes();
|
|
1409
|
-
const entryIndex = snapshotIndexes?.get(entry);
|
|
1410
|
-
return entryIndex !== void 0 && firstUserIndex >= 0 && firstUserIndex < entryIndex && lastAssistantIndex <= entryIndex;
|
|
1411
|
-
};
|
|
1412
|
-
const isLastSameRunAssistantRow = (entry) => {
|
|
1534
|
+
const hasTerminalSnapshotPosition = (entry, requireUser) => {
|
|
1413
1535
|
const runId = current.identity?.runId;
|
|
1414
1536
|
if (!runId || entry.identity?.runId !== runId) return false;
|
|
1415
1537
|
ensureRunIndexes();
|
|
1416
1538
|
const entryIndex = snapshotIndexes?.get(entry);
|
|
1417
|
-
return entryIndex !== void 0 && lastAssistantIndex >= 0 && entryIndex >=
|
|
1539
|
+
return entryIndex !== void 0 && lastAssistantIndex <= entryIndex && (requireUser ? firstUserIndex >= 0 && firstUserIndex < entryIndex : lastAssistantIndex >= 0);
|
|
1418
1540
|
};
|
|
1419
1541
|
const durableTerminalMatches = matches.filter((entry) => {
|
|
1420
|
-
return (asNullableRecord(asNullableRecord(entry.message)?.["__openclaw"])?.runTerminal === true || entry.identity?.runId === current.identity?.runId && (hasTerminalStopReason(entry.message) ||
|
|
1542
|
+
return (asNullableRecord(asNullableRecord(entry.message)?.["__openclaw"])?.runTerminal === true || entry.identity?.runId === current.identity?.runId && (hasTerminalStopReason(entry.message) || isToolUsePersistedFinalRow(entry.message) && hasTerminalSnapshotPosition(entry, false)) || hasTerminalSnapshotPosition(entry, true)) && readFinalContentIdentity(entry.message) === terminalContent;
|
|
1421
1543
|
});
|
|
1422
1544
|
const entry = durableTerminalMatches.length === 1 ? durableTerminalMatches[0] : void 0;
|
|
1423
1545
|
if (!entry) return null;
|
|
@@ -1447,22 +1569,19 @@ function retainSessionProjectionRuns(runs) {
|
|
|
1447
1569
|
}
|
|
1448
1570
|
//#endregion
|
|
1449
1571
|
//#region packages/gateway-client/src/session-projection-run-event.ts
|
|
1450
|
-
function readNonemptyString(value) {
|
|
1451
|
-
return typeof value === "string" ? value.trim() || null : null;
|
|
1452
|
-
}
|
|
1453
1572
|
/** Normalizes Gateway run envelopes once for every browser and terminal adapter. */
|
|
1454
1573
|
function reduceSessionProjectionRunEvent(projection, event, scope = {}) {
|
|
1455
|
-
const runId =
|
|
1574
|
+
const runId = readSessionProjectionString(event.runId);
|
|
1456
1575
|
if (!runId || typeof event.state !== "string" || ![
|
|
1457
1576
|
"delta",
|
|
1458
1577
|
"final",
|
|
1459
1578
|
"error",
|
|
1460
1579
|
"aborted"
|
|
1461
1580
|
].includes(event.state)) return null;
|
|
1462
|
-
const message = event.message;
|
|
1463
|
-
const messageStopReason = isRecord(message) ?
|
|
1464
|
-
const stopReason =
|
|
1465
|
-
const errorKind =
|
|
1581
|
+
const message = event.state === "delta" ? mergeChatStreamMessage(projection.runs[runId]?.message, event) : event.message;
|
|
1582
|
+
const messageStopReason = isRecord(message) ? readSessionProjectionString(message.stopReason) : null;
|
|
1583
|
+
const stopReason = readSessionProjectionString(event.stopReason) ?? messageStopReason;
|
|
1584
|
+
const errorKind = readSessionProjectionString(event.errorKind);
|
|
1466
1585
|
const base = {
|
|
1467
1586
|
runId,
|
|
1468
1587
|
seq: typeof event.seq === "number" ? event.seq : void 0,
|
|
@@ -1815,16 +1934,67 @@ function reduceSessionProjection(state, event) {
|
|
|
1815
1934
|
}
|
|
1816
1935
|
}
|
|
1817
1936
|
//#endregion
|
|
1818
|
-
//#region packages/
|
|
1819
|
-
|
|
1937
|
+
//#region packages/normalization-core/src/result.ts
|
|
1938
|
+
/** Create a successful {@link Result}. */
|
|
1939
|
+
function ok(value) {
|
|
1940
|
+
return {
|
|
1941
|
+
ok: true,
|
|
1942
|
+
value
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
/** Create a failed {@link Result}. */
|
|
1946
|
+
function err(error) {
|
|
1820
1947
|
return {
|
|
1821
|
-
|
|
1822
|
-
|
|
1948
|
+
ok: false,
|
|
1949
|
+
error
|
|
1823
1950
|
};
|
|
1824
1951
|
}
|
|
1952
|
+
//#endregion
|
|
1953
|
+
//#region packages/normalization-core/src/agent-id.ts
|
|
1954
|
+
const VALID_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
|
|
1955
|
+
const INVALID_CHARS_RE = /[^a-z0-9_-]+/g;
|
|
1956
|
+
const LEADING_DASH_RE = /^-+/;
|
|
1957
|
+
const TRAILING_DASH_RE = /-+$/;
|
|
1958
|
+
/** Normalizes an explicitly supplied agent id without falling back to the default agent. */
|
|
1959
|
+
function normalizeAgentIdStrict(value) {
|
|
1960
|
+
const trimmed = (value ?? "").trim();
|
|
1961
|
+
const normalized = trimmed.toLowerCase();
|
|
1962
|
+
if (VALID_ID_RE.test(trimmed)) return ok(normalized);
|
|
1963
|
+
const agentId = normalized.replace(INVALID_CHARS_RE, "-").replace(LEADING_DASH_RE, "").replace(TRAILING_DASH_RE, "").slice(0, 64);
|
|
1964
|
+
return agentId ? ok(agentId) : err("unrepresentable");
|
|
1965
|
+
}
|
|
1966
|
+
//#endregion
|
|
1967
|
+
//#region packages/normalization-core/src/uuid.ts
|
|
1968
|
+
let warnedWeakCrypto = false;
|
|
1969
|
+
function warnWeakCryptoOnce() {
|
|
1970
|
+
if (warnedWeakCrypto) return;
|
|
1971
|
+
warnedWeakCrypto = true;
|
|
1972
|
+
console.warn("[uuid] crypto API missing; refusing insecure UUID generation");
|
|
1973
|
+
}
|
|
1974
|
+
function generateUUID(cryptoLike = globalThis.crypto) {
|
|
1975
|
+
if (cryptoLike && typeof cryptoLike.randomUUID === "function") return cryptoLike.randomUUID();
|
|
1976
|
+
if (cryptoLike && typeof cryptoLike.getRandomValues === "function") {
|
|
1977
|
+
const bytes = /* @__PURE__ */ new Uint8Array(16);
|
|
1978
|
+
cryptoLike.getRandomValues(bytes);
|
|
1979
|
+
const view = new DataView(bytes.buffer);
|
|
1980
|
+
view.setUint8(6, view.getUint8(6) & 15 | 64);
|
|
1981
|
+
view.setUint8(8, view.getUint8(8) & 63 | 128);
|
|
1982
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1983
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
1984
|
+
}
|
|
1985
|
+
warnWeakCryptoOnce();
|
|
1986
|
+
throw new Error("Web Crypto is required for UUID generation");
|
|
1987
|
+
}
|
|
1988
|
+
//#endregion
|
|
1989
|
+
//#region packages/gateway-client/src/session-subscriptions.ts
|
|
1990
|
+
function normalizedAgentScope(agentId) {
|
|
1991
|
+
if (!agentId) return null;
|
|
1992
|
+
const normalized = normalizeAgentIdStrict(agentId);
|
|
1993
|
+
return normalized.ok ? normalized.value : agentId;
|
|
1994
|
+
}
|
|
1825
1995
|
/**
|
|
1826
|
-
*
|
|
1827
|
-
*
|
|
1996
|
+
* Shared leases retain one wire observer ID. The Gateway combines independently
|
|
1997
|
+
* addressed observers; approval delivery upgrades the same owner.
|
|
1828
1998
|
*/
|
|
1829
1999
|
var GatewaySessionMessageSubscriptionCoordinator = class {
|
|
1830
2000
|
#client;
|
|
@@ -1846,17 +2016,24 @@ var GatewaySessionMessageSubscriptionCoordinator = class {
|
|
|
1846
2016
|
const normalizedKey = key.trim();
|
|
1847
2017
|
if (!normalizedKey) throw new Error("Session message subscription requires a session key");
|
|
1848
2018
|
const agentId = options.agentId?.trim() || null;
|
|
2019
|
+
const scopeAgentId = normalizedAgentScope(agentId);
|
|
2020
|
+
const narration = options.mode === "narration";
|
|
2021
|
+
const includeApprovals = options.includeApprovals === true;
|
|
1849
2022
|
let entry;
|
|
2023
|
+
let created = false;
|
|
1850
2024
|
while (true) {
|
|
1851
2025
|
if (this.#retired) throw new Error("Session message subscription belongs to a replaced Gateway connection");
|
|
1852
|
-
const existing = [...this.#entries].find((candidate) =>
|
|
2026
|
+
const existing = [...this.#entries].find((candidate) => {
|
|
2027
|
+
return candidate.scopeAgentId === scopeAgentId && ([...candidate.requestedKeys].some((requestedKey) => this.#areKeysEquivalent(requestedKey, normalizedKey)) || candidate.key !== "global" && this.#areKeysEquivalent(candidate.key, normalizedKey));
|
|
2028
|
+
});
|
|
1853
2029
|
if (!existing) {
|
|
1854
|
-
const provisional = [...this.#entries].find((candidate) => candidate.
|
|
2030
|
+
const provisional = [...this.#entries].find((candidate) => candidate.scopeAgentId === scopeAgentId && !candidate.canonicalSettled && this.#couldShareCanonicalIdentity(candidate.key, normalizedKey));
|
|
1855
2031
|
if (provisional) {
|
|
1856
2032
|
await (provisional.plainFallback ?? provisional.ready).catch(() => void 0);
|
|
1857
2033
|
continue;
|
|
1858
2034
|
}
|
|
1859
|
-
entry = this.#createEntry(normalizedKey, agentId
|
|
2035
|
+
entry = this.#createEntry(normalizedKey, agentId);
|
|
2036
|
+
created = true;
|
|
1860
2037
|
break;
|
|
1861
2038
|
}
|
|
1862
2039
|
if (!existing.release) {
|
|
@@ -1867,13 +2044,31 @@ var GatewaySessionMessageSubscriptionCoordinator = class {
|
|
|
1867
2044
|
await existing.release.catch(() => void 0);
|
|
1868
2045
|
}
|
|
1869
2046
|
entry.pendingOwners += 1;
|
|
2047
|
+
if (!narration) entry.pendingFullOwners += 1;
|
|
1870
2048
|
try {
|
|
1871
|
-
|
|
2049
|
+
if (created) {
|
|
2050
|
+
entry.ready = this.#requestSubscribe(entry, includeApprovals);
|
|
2051
|
+
if (includeApprovals) entry.ready = this.#trackApprovalRequest(entry, entry.ready);
|
|
2052
|
+
entry.ready.catch(() => void 0);
|
|
2053
|
+
}
|
|
2054
|
+
if (entry.refreshRequired) {
|
|
2055
|
+
entry.refreshRequired = false;
|
|
2056
|
+
entry.plainFallback = null;
|
|
2057
|
+
entry.approvalRequest = null;
|
|
2058
|
+
const retainedApprovals = [...entry.handles].some((handle) => handle.includeApprovals);
|
|
2059
|
+
entry.ready = this.#requestSubscribe(entry, retainedApprovals).catch((error) => {
|
|
2060
|
+
entry.refreshRequired = true;
|
|
2061
|
+
throw error;
|
|
2062
|
+
});
|
|
2063
|
+
}
|
|
2064
|
+
const result = await this.#acquireCapability(entry, includeApprovals);
|
|
2065
|
+
if (!narration && entry.mode === "narration") await this.#requestSubscribe(entry, false, "full");
|
|
1872
2066
|
if (this.#retired) throw new Error("Session message subscription completed on a replaced Gateway connection");
|
|
1873
2067
|
const subscription = {
|
|
1874
2068
|
key: result.key,
|
|
1875
2069
|
agentId,
|
|
1876
|
-
...
|
|
2070
|
+
...narration ? { mode: "narration" } : {},
|
|
2071
|
+
...includeApprovals ? {
|
|
1877
2072
|
includeApprovals: true,
|
|
1878
2073
|
...result.approvalReplay !== void 0 ? { approvalReplay: result.approvalReplay } : {}
|
|
1879
2074
|
} : {}
|
|
@@ -1886,6 +2081,7 @@ var GatewaySessionMessageSubscriptionCoordinator = class {
|
|
|
1886
2081
|
return subscription;
|
|
1887
2082
|
} finally {
|
|
1888
2083
|
entry.pendingOwners -= 1;
|
|
2084
|
+
if (!narration) entry.pendingFullOwners -= 1;
|
|
1889
2085
|
if (entry.pendingOwners === 0 && entry.handles.size === 0 && !entry.release) this.#entries.delete(entry);
|
|
1890
2086
|
}
|
|
1891
2087
|
}
|
|
@@ -1893,26 +2089,34 @@ var GatewaySessionMessageSubscriptionCoordinator = class {
|
|
|
1893
2089
|
const owner = sessionMessageSubscriptionOwners.get(subscription);
|
|
1894
2090
|
if (!owner || owner.coordinator !== this) return Promise.resolve();
|
|
1895
2091
|
const { entry } = owner;
|
|
1896
|
-
if (
|
|
2092
|
+
if (entry.release) return entry.releasing === subscription ? entry.release : entry.release.catch(() => void 0).then(() => this.release(subscription));
|
|
2093
|
+
const releasesLastFullOwner = subscription.mode !== "narration" && [...entry.handles].every((handle) => handle === subscription || handle.mode === "narration");
|
|
2094
|
+
if (this.#retired || entry.handles.size > 1 && !releasesLastFullOwner) {
|
|
1897
2095
|
this.#finishRelease(subscription, owner);
|
|
1898
2096
|
return Promise.resolve();
|
|
1899
2097
|
}
|
|
1900
|
-
if (entry.release) return entry.release;
|
|
1901
2098
|
if (entry.pendingOwners > 0) {
|
|
1902
|
-
const
|
|
1903
|
-
|
|
2099
|
+
const tracked = Promise.allSettled([
|
|
2100
|
+
entry.ready,
|
|
2101
|
+
entry.approvalRequest,
|
|
2102
|
+
entry.wireRequest
|
|
2103
|
+
]).then(() => {
|
|
1904
2104
|
if (entry.release === tracked) entry.release = null;
|
|
1905
2105
|
return this.release(subscription);
|
|
1906
2106
|
});
|
|
1907
2107
|
entry.release = tracked;
|
|
2108
|
+
entry.releasing = subscription;
|
|
1908
2109
|
return tracked;
|
|
1909
2110
|
}
|
|
1910
|
-
const
|
|
1911
|
-
|
|
1912
|
-
|
|
2111
|
+
const removeEntry = entry.handles.size === 1;
|
|
2112
|
+
const tracked = (removeEntry ? this.#requestMessages(entry).catch((error) => {
|
|
2113
|
+
if (error instanceof GatewayProtocolRequestTimeoutError && error.requestSent) entry.refreshRequired = true;
|
|
2114
|
+
throw error;
|
|
2115
|
+
}) : this.#requestSubscribe(entry, false, "narration")).then(() => this.#finishRelease(subscription, owner, removeEntry)).finally(() => {
|
|
1913
2116
|
if (entry.release === tracked) entry.release = null;
|
|
1914
2117
|
});
|
|
1915
2118
|
entry.release = tracked;
|
|
2119
|
+
entry.releasing = subscription;
|
|
1916
2120
|
return tracked;
|
|
1917
2121
|
}
|
|
1918
2122
|
/** A reconnect retires leases without touching the next connection's observers. */
|
|
@@ -1924,22 +2128,26 @@ var GatewaySessionMessageSubscriptionCoordinator = class {
|
|
|
1924
2128
|
}
|
|
1925
2129
|
this.#entries.clear();
|
|
1926
2130
|
}
|
|
1927
|
-
#createEntry(key, agentId
|
|
2131
|
+
#createEntry(key, agentId) {
|
|
1928
2132
|
const entry = {
|
|
2133
|
+
subscriptionId: generateUUID(),
|
|
1929
2134
|
key,
|
|
1930
2135
|
requestedKeys: /* @__PURE__ */ new Set([key]),
|
|
1931
2136
|
agentId,
|
|
2137
|
+
scopeAgentId: normalizedAgentScope(agentId),
|
|
2138
|
+
ownerAgentId: null,
|
|
1932
2139
|
ready: Promise.resolve({ key }),
|
|
1933
2140
|
approvalRequest: null,
|
|
1934
2141
|
plainFallback: null,
|
|
2142
|
+
wireRequest: null,
|
|
2143
|
+
includeApprovals: false,
|
|
1935
2144
|
canonicalSettled: false,
|
|
2145
|
+
refreshRequired: false,
|
|
1936
2146
|
handles: /* @__PURE__ */ new Set(),
|
|
1937
2147
|
pendingOwners: 0,
|
|
2148
|
+
pendingFullOwners: 0,
|
|
1938
2149
|
release: null
|
|
1939
2150
|
};
|
|
1940
|
-
entry.ready = this.#requestSubscribe(entry, includeApprovals);
|
|
1941
|
-
if (includeApprovals) entry.ready = this.#trackApprovalRequest(entry, entry.ready);
|
|
1942
|
-
entry.ready.catch(() => void 0);
|
|
1943
2151
|
this.#entries.add(entry);
|
|
1944
2152
|
return entry;
|
|
1945
2153
|
}
|
|
@@ -1970,19 +2178,43 @@ var GatewaySessionMessageSubscriptionCoordinator = class {
|
|
|
1970
2178
|
entry.approvalRequest = tracked;
|
|
1971
2179
|
return tracked;
|
|
1972
2180
|
}
|
|
1973
|
-
|
|
1974
|
-
const
|
|
1975
|
-
const
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
2181
|
+
#requestSubscribe(entry, includeApprovals, mode) {
|
|
2182
|
+
const send = () => this.#sendSubscribe(entry, includeApprovals, mode);
|
|
2183
|
+
const tracked = (entry.wireRequest ? entry.wireRequest.then(send, send) : send()).finally(() => {
|
|
2184
|
+
if (entry.wireRequest === tracked) entry.wireRequest = null;
|
|
2185
|
+
});
|
|
2186
|
+
entry.wireRequest = tracked;
|
|
2187
|
+
return tracked;
|
|
2188
|
+
}
|
|
2189
|
+
#requestMessages(entry, subscription) {
|
|
2190
|
+
const agentId = entry.key === "global" ? entry.ownerAgentId ?? entry.agentId : entry.agentId;
|
|
2191
|
+
return this.#client.request(subscription ? "sessions.messages.subscribe" : "sessions.messages.unsubscribe", {
|
|
2192
|
+
subscriptionId: entry.subscriptionId,
|
|
2193
|
+
key: entry.key,
|
|
2194
|
+
...agentId ? { agentId } : {},
|
|
2195
|
+
...subscription?.mode ? { mode: subscription.mode } : {},
|
|
2196
|
+
...subscription?.includeApprovals ? { includeApprovals: true } : {}
|
|
2197
|
+
}, { timeoutMs: DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS });
|
|
2198
|
+
}
|
|
2199
|
+
async #sendSubscribe(entry, requestedApprovals, requestedMode) {
|
|
2200
|
+
if (this.#retired) throw new Error("Session message subscription belongs to a replaced Gateway connection");
|
|
2201
|
+
if (requestedMode === "full" && entry.mode !== "narration") return { key: entry.key };
|
|
2202
|
+
const includeApprovals = requestedApprovals || requestedMode !== void 0 && entry.includeApprovals;
|
|
2203
|
+
const mode = requestedMode === "narration" || requestedMode !== "full" && entry.pendingFullOwners === 0 && [...entry.handles].every((handle) => handle.mode === "narration") ? "narration" : void 0;
|
|
2204
|
+
const result = await this.#requestMessages(entry, {
|
|
2205
|
+
mode,
|
|
2206
|
+
includeApprovals
|
|
2207
|
+
}).catch(async (error) => {
|
|
1979
2208
|
if (!(error instanceof GatewayProtocolRequestTimeoutError) || !error.requestSent || this.#retired) throw error;
|
|
1980
2209
|
try {
|
|
1981
2210
|
const retainedApprovals = [...entry.handles].some((handle) => handle.includeApprovals);
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
2211
|
+
const retainedMode = entry.handles.size > 0 && [...entry.handles].every((handle) => handle.mode === "narration") ? "narration" : void 0;
|
|
2212
|
+
await this.#requestMessages(entry, entry.handles.size > 0 ? {
|
|
2213
|
+
mode: retainedMode,
|
|
2214
|
+
includeApprovals: retainedApprovals
|
|
2215
|
+
} : void 0);
|
|
2216
|
+
entry.mode = retainedMode;
|
|
2217
|
+
entry.includeApprovals = retainedApprovals;
|
|
1986
2218
|
} catch (recoveryError) {
|
|
1987
2219
|
if (!this.#retired) throw new AggregateError([error, recoveryError], "session message subscription recovery failed", { cause: recoveryError });
|
|
1988
2220
|
}
|
|
@@ -1992,6 +2224,10 @@ var GatewaySessionMessageSubscriptionCoordinator = class {
|
|
|
1992
2224
|
const responseKey = response && "key" in response ? response.key : void 0;
|
|
1993
2225
|
entry.key = typeof responseKey === "string" && responseKey.trim() ? responseKey.trim() : entry.key;
|
|
1994
2226
|
entry.canonicalSettled = true;
|
|
2227
|
+
const responseAgentId = response && "agentId" in response ? response.agentId : void 0;
|
|
2228
|
+
entry.ownerAgentId = normalizedAgentScope((typeof responseAgentId === "string" ? responseAgentId : null) ?? entry.ownerAgentId ?? entry.scopeAgentId);
|
|
2229
|
+
entry.mode = mode;
|
|
2230
|
+
entry.includeApprovals = includeApprovals;
|
|
1995
2231
|
return {
|
|
1996
2232
|
key: entry.key,
|
|
1997
2233
|
...response && "approvalReplay" in response ? { approvalReplay: response.approvalReplay } : {}
|
|
@@ -2029,4 +2265,4 @@ function releaseGatewaySessionMessageSubscription(subscription) {
|
|
|
2029
2265
|
return sessionMessageSubscriptionOwners.get(subscription)?.coordinator.release(subscription) ?? Promise.resolve();
|
|
2030
2266
|
}
|
|
2031
2267
|
//#endregion
|
|
2032
|
-
export { selectGatewayConnectAuth as A, isGatewayProtocolResponseError as C, GatewayBrowserDeviceAuthLifecycle as D, gatewayOriginScope as E, buildDeviceAuthPayload as
|
|
2268
|
+
export { selectGatewayConnectAuth as A, isGatewayProtocolResponseError as C, GatewayBrowserDeviceAuthLifecycle as D, gatewayOriginScope as E, isRecord as F, buildDeviceAuthPayload as I, buildDeviceAuthPayloadV3 as L, mergeChatStreamMessage as M, asNullableRecord as N, buildGatewayConnectAuth as O, asRecord as P, normalizeDeviceMetadataForAuth as R, GatewayProtocolRequestTimeoutError as S, gatewayCredentialScope as T, readSessionMessageIdentity as _, createSessionProjection as a, GatewayProtocolClient as b, reduceSessionProjection as c, readSessionProjectionFinalMessageIdentity as d, isSessionProjectionErrorMessage as f, readAssistantStreamSegmentIdentity as g, normalizeSessionProjectionRunId as h, resetGatewaySessionMessageSubscriptionCoordinator as i, shouldRetryGatewayWithDeviceToken as j, resolveGatewayConnectScopes as k, reduceSessionProjectionRunEvent as l, isLocallyOptimisticSessionMessage as m, getGatewaySessionMessageSubscriptionCoordinator as n, projectLiveSessionMessage as o, projectSessionTerminalReplyMessage as p, releaseGatewaySessionMessageSubscription as r, reconcileSessionProjectionSnapshot as s, GatewaySessionMessageSubscriptionCoordinator as t, hasSessionProjectionAcceptedFinal as u, readSessionMessageSequence as v, resolveModelCatalogConnect as w, GatewayProtocolRequestError as x, shouldPauseGatewayReconnect as y };
|