@openclaw/gateway-client 2026.9.3 → 2026.9.5
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 +35 -0
- package/dist/browser.d.mts +13 -4
- package/dist/browser.mjs +3 -2
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +247 -126
- package/dist/{protocol-client-DbX7wQED.d.mts → protocol-client-DZ7F1DIs.d.mts} +14 -5
- package/dist/{readiness-uQKoJkl5.d.mts → readiness-B99dV5nY.d.mts} +44 -25
- package/dist/readiness.d.mts +1 -1
- package/dist/{session-subscriptions-DwU8T0CH.mjs → session-subscriptions-DrOaOXNR.mjs} +417 -115
- package/dist/{session-subscriptions-C3M4jnCd.d.mts → session-subscriptions-yRMtCvZZ.d.mts} +27 -15
- package/package.json +2 -2
|
@@ -225,6 +225,19 @@ function gatewayCredentialScope(gatewayUrl) {
|
|
|
225
225
|
return normalizeGatewayScope(gatewayUrl, true);
|
|
226
226
|
}
|
|
227
227
|
//#endregion
|
|
228
|
+
//#region packages/gateway-client/src/model-catalog-connect.ts
|
|
229
|
+
const MODEL_CATALOG_SNAPSHOT = "model-catalog-snapshot";
|
|
230
|
+
/** A requested snapshot opts in only after the server advertises its connect field. */
|
|
231
|
+
function resolveModelCatalogConnect(params) {
|
|
232
|
+
const modelCatalog = params.serverCapabilities.includes(MODEL_CATALOG_SNAPSHOT) ? params.modelCatalog : void 0;
|
|
233
|
+
const caps = params.caps?.filter((cap) => cap !== MODEL_CATALOG_SNAPSHOT);
|
|
234
|
+
if (modelCatalog === void 0) return { caps };
|
|
235
|
+
return {
|
|
236
|
+
modelCatalog,
|
|
237
|
+
caps: [...caps ?? [], MODEL_CATALOG_SNAPSHOT]
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
//#endregion
|
|
228
241
|
//#region packages/retry/src/index.ts
|
|
229
242
|
const MAX_TIMER_TIMEOUT_MS = 2147e6;
|
|
230
243
|
function computeBackoff(policy, attempt) {
|
|
@@ -306,9 +319,16 @@ const DEFAULT_RETRY_CONFIG = {
|
|
|
306
319
|
maxDelayMs: 3e4,
|
|
307
320
|
jitter: 0
|
|
308
321
|
};
|
|
309
|
-
const defaultSleep = (ms) =>
|
|
310
|
-
|
|
311
|
-
|
|
322
|
+
const defaultSleep = async (ms) => {
|
|
323
|
+
let remainingMs = ms;
|
|
324
|
+
do {
|
|
325
|
+
const delayMs = Math.min(remainingMs, MAX_TIMER_TIMEOUT_MS);
|
|
326
|
+
await new Promise((resolve) => {
|
|
327
|
+
setTimeout(resolve, delayMs);
|
|
328
|
+
});
|
|
329
|
+
remainingMs -= delayMs;
|
|
330
|
+
} while (remainingMs > 0);
|
|
331
|
+
};
|
|
312
332
|
function clampNumber(value, fallback, min, max) {
|
|
313
333
|
const next = Number.isFinite(value) ? value : void 0;
|
|
314
334
|
if (next === void 0) return fallback;
|
|
@@ -630,8 +650,10 @@ var GatewayProtocolClient = class {
|
|
|
630
650
|
this.listeners = new GatewayEventListeners();
|
|
631
651
|
this.stopped = true;
|
|
632
652
|
this.generation = 0;
|
|
653
|
+
this.connectionAbort = null;
|
|
633
654
|
this.lastSeq = null;
|
|
634
655
|
this.connectNonce = null;
|
|
656
|
+
this.serverCapabilities = [];
|
|
635
657
|
this.connectSent = false;
|
|
636
658
|
this.connectRequestSent = false;
|
|
637
659
|
this.handshakeTimer = null;
|
|
@@ -676,6 +698,7 @@ var GatewayProtocolClient = class {
|
|
|
676
698
|
}
|
|
677
699
|
stop() {
|
|
678
700
|
this.stopped = true;
|
|
701
|
+
this.connectionAbort?.abort();
|
|
679
702
|
this.clearHandshakeTimer();
|
|
680
703
|
this.reconnectSignal = null;
|
|
681
704
|
this.reconnectSupervisor.reset();
|
|
@@ -700,6 +723,7 @@ var GatewayProtocolClient = class {
|
|
|
700
723
|
return this.listeners.add(listener);
|
|
701
724
|
}
|
|
702
725
|
closeSocket(code, reason) {
|
|
726
|
+
this.connectionAbort?.abort();
|
|
703
727
|
this.socket?.close(code, reason);
|
|
704
728
|
}
|
|
705
729
|
resetReconnectBackoff(initialMs) {
|
|
@@ -731,6 +755,7 @@ var GatewayProtocolClient = class {
|
|
|
731
755
|
this.lastSeq = null;
|
|
732
756
|
this.connectNonce = null;
|
|
733
757
|
this.connectChallengeTs = void 0;
|
|
758
|
+
this.serverCapabilities = [];
|
|
734
759
|
this.connectSent = this.connectRequestSent = false;
|
|
735
760
|
this.socketOpened = false;
|
|
736
761
|
this.helloReceived = false;
|
|
@@ -756,6 +781,7 @@ var GatewayProtocolClient = class {
|
|
|
756
781
|
return;
|
|
757
782
|
}
|
|
758
783
|
this.generation = generation;
|
|
784
|
+
this.connectionAbort = new AbortController();
|
|
759
785
|
this.socket = socket;
|
|
760
786
|
const now = this.nowMs();
|
|
761
787
|
this.connectTiming = {
|
|
@@ -806,7 +832,9 @@ var GatewayProtocolClient = class {
|
|
|
806
832
|
planOrPromise = this.opts.buildConnectPlan({
|
|
807
833
|
nonce: this.connectNonce,
|
|
808
834
|
challengeTs: this.connectChallengeTs,
|
|
809
|
-
|
|
835
|
+
serverCapabilities: this.serverCapabilities,
|
|
836
|
+
generation,
|
|
837
|
+
...this.connectAuthority(socket, generation)
|
|
810
838
|
});
|
|
811
839
|
} catch (error) {
|
|
812
840
|
this.handleConnectPlanError(socket, generation, error);
|
|
@@ -819,7 +847,7 @@ var GatewayProtocolClient = class {
|
|
|
819
847
|
this.sendConnectPlan(socket, generation, planOrPromise);
|
|
820
848
|
}
|
|
821
849
|
handleConnectPlanError(socket, generation, error) {
|
|
822
|
-
if (!this.
|
|
850
|
+
if (!this.isConnectCurrent(socket, generation)) return;
|
|
823
851
|
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
824
852
|
const outcome = this.opts.onConnectPlanError?.(normalized) ?? {
|
|
825
853
|
closeCode: 1008,
|
|
@@ -830,8 +858,9 @@ var GatewayProtocolClient = class {
|
|
|
830
858
|
socket.close(outcome.closeCode, outcome.closeReason);
|
|
831
859
|
}
|
|
832
860
|
sendConnectPlan(socket, generation, plan) {
|
|
833
|
-
if (!this.
|
|
861
|
+
if (!this.isConnectCurrent(socket, generation)) return;
|
|
834
862
|
const context = {
|
|
863
|
+
...this.connectAuthority(socket, generation),
|
|
835
864
|
generation,
|
|
836
865
|
nonce: this.connectNonce,
|
|
837
866
|
challengeTs: this.connectChallengeTs,
|
|
@@ -841,27 +870,43 @@ var GatewayProtocolClient = class {
|
|
|
841
870
|
this.recordTiming("request-sent", generation, plan);
|
|
842
871
|
this.connectRequestSent = true;
|
|
843
872
|
this.request("connect", this.opts.buildConnectParams(plan)).then((hello) => {
|
|
844
|
-
if (!this.
|
|
873
|
+
if (!this.isConnectCurrent(socket, generation)) return;
|
|
845
874
|
this.helloReceived = true;
|
|
846
875
|
this.clearHandshakeTimer();
|
|
847
876
|
this.connectFailure = void 0;
|
|
848
877
|
this.reconnectSupervisor.reset();
|
|
849
878
|
this.recordTiming("hello", generation, plan);
|
|
850
|
-
|
|
851
|
-
|
|
879
|
+
const publishHello = () => {
|
|
880
|
+
if (!this.isConnectCurrent(socket, generation)) return;
|
|
881
|
+
this.invoke("hello", () => this.opts.onHello?.(hello));
|
|
882
|
+
};
|
|
883
|
+
const accepted = this.opts.onConnectHello?.(hello, context);
|
|
884
|
+
if (accepted instanceof Promise) return accepted.then(publishHello);
|
|
885
|
+
return publishHello();
|
|
852
886
|
}).catch((error) => {
|
|
853
887
|
if (!this.isActive(socket, generation)) return;
|
|
854
888
|
const requestError = error instanceof GatewayProtocolRequestError ? error : new GatewayProtocolRequestError({ message: String(error) });
|
|
889
|
+
this.connectFailure = { error: requestError };
|
|
855
890
|
const outcome = this.opts.onConnectFailure?.(requestError, context) ?? {
|
|
856
891
|
closeCode: 1008,
|
|
857
892
|
closeReason: "connect failed"
|
|
858
893
|
};
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
894
|
+
const applyFailure = (decision) => {
|
|
895
|
+
if (!this.isActive(socket, generation)) return;
|
|
896
|
+
this.connectFailure = {
|
|
897
|
+
error: requestError,
|
|
898
|
+
reconnectDelayMs: decision.reconnectDelayMs
|
|
899
|
+
};
|
|
900
|
+
if (decision.stop) this.stopped = true;
|
|
901
|
+
this.connectionAbort?.abort();
|
|
902
|
+
socket.close(decision.closeCode, decision.closeReason);
|
|
862
903
|
};
|
|
863
|
-
if (outcome
|
|
864
|
-
|
|
904
|
+
if (outcome instanceof Promise) return outcome.then(applyFailure);
|
|
905
|
+
return applyFailure(outcome);
|
|
906
|
+
}).catch((error) => {
|
|
907
|
+
if (!this.isConnectCurrent(socket, generation)) return;
|
|
908
|
+
this.opts.onConnectError?.(error instanceof Error ? error : new Error(String(error)));
|
|
909
|
+
this.closeSocket(1008, "connect failed");
|
|
865
910
|
});
|
|
866
911
|
}
|
|
867
912
|
handleMessage(socket, generation, raw) {
|
|
@@ -887,6 +932,7 @@ var GatewayProtocolClient = class {
|
|
|
887
932
|
return;
|
|
888
933
|
}
|
|
889
934
|
this.connectNonce = nonce;
|
|
935
|
+
this.serverCapabilities = Array.isArray(payload?.capabilities) ? payload.capabilities.filter((value) => typeof value === "string") : [];
|
|
890
936
|
const challengeTs = payload?.ts;
|
|
891
937
|
this.connectChallengeTs = typeof challengeTs === "number" && Number.isSafeInteger(challengeTs) && challengeTs >= 0 ? challengeTs : null;
|
|
892
938
|
this.recordTiming("challenge", generation);
|
|
@@ -934,6 +980,7 @@ var GatewayProtocolClient = class {
|
|
|
934
980
|
return;
|
|
935
981
|
}
|
|
936
982
|
this.socket = null;
|
|
983
|
+
this.connectionAbort?.abort();
|
|
937
984
|
this.clearHandshakeTimer();
|
|
938
985
|
const context = {
|
|
939
986
|
...this.closeContext(),
|
|
@@ -978,6 +1025,20 @@ var GatewayProtocolClient = class {
|
|
|
978
1025
|
connectFailure: this.connectFailure
|
|
979
1026
|
};
|
|
980
1027
|
}
|
|
1028
|
+
isConnectCurrent(socket, generation) {
|
|
1029
|
+
return this.isActive(socket, generation) && socket.isOpen() && !this.connectionAbort?.signal.aborted;
|
|
1030
|
+
}
|
|
1031
|
+
connectAuthority(socket, generation) {
|
|
1032
|
+
const signal = this.connectionAbort?.signal;
|
|
1033
|
+
if (!signal) throw new Error("gateway connection authority is unavailable");
|
|
1034
|
+
return {
|
|
1035
|
+
signal,
|
|
1036
|
+
assertCurrent: () => {
|
|
1037
|
+
signal.throwIfAborted();
|
|
1038
|
+
if (!this.isConnectCurrent(socket, generation)) throw new Error("gateway connection retired");
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
981
1042
|
isActive(socket, generation) {
|
|
982
1043
|
return !this.stopped && this.socket === socket && this.generation === generation;
|
|
983
1044
|
}
|
|
@@ -1032,6 +1093,67 @@ function asNullableRecord(value) {
|
|
|
1032
1093
|
return isRecord(value) ? value : null;
|
|
1033
1094
|
}
|
|
1034
1095
|
//#endregion
|
|
1096
|
+
//#region packages/normalization-core/src/stable-stringify.ts
|
|
1097
|
+
const preserveString = (value) => value;
|
|
1098
|
+
/** Deterministically stringifies values, optionally normalizing strings before key ordering. */
|
|
1099
|
+
function stableStringify(value, normalizeString = preserveString) {
|
|
1100
|
+
return stringifyStableValue(value, /* @__PURE__ */ new WeakSet(), normalizeString);
|
|
1101
|
+
}
|
|
1102
|
+
function stringifyStableValue(value, stack, normalizeString) {
|
|
1103
|
+
if (value === null || value === void 0) return String(value);
|
|
1104
|
+
if (typeof value === "number" && !Number.isFinite(value)) return JSON.stringify(String(value));
|
|
1105
|
+
if (typeof value === "bigint") return JSON.stringify(value.toString());
|
|
1106
|
+
if (typeof value === "string") return JSON.stringify(normalizeString(value));
|
|
1107
|
+
if (typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
1108
|
+
if (stack.has(value)) return JSON.stringify("[Circular]");
|
|
1109
|
+
stack.add(value);
|
|
1110
|
+
try {
|
|
1111
|
+
return stringifyObjectValue(value, stack, normalizeString);
|
|
1112
|
+
} finally {
|
|
1113
|
+
stack.delete(value);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
function stringifyObjectValue(value, stack, normalizeString) {
|
|
1117
|
+
if (value instanceof Error) return stringifyStableValue({
|
|
1118
|
+
name: value.name,
|
|
1119
|
+
message: value.message,
|
|
1120
|
+
stack: value.stack
|
|
1121
|
+
}, stack, normalizeString);
|
|
1122
|
+
if (value instanceof Uint8Array) return stringifyStableValue({
|
|
1123
|
+
type: "Uint8Array",
|
|
1124
|
+
data: encodeBase64(value)
|
|
1125
|
+
}, stack, normalizeString);
|
|
1126
|
+
if (Array.isArray(value)) {
|
|
1127
|
+
const serializedEntries = [];
|
|
1128
|
+
for (const entry of value) serializedEntries.push(stringifyStableValue(entry, stack, normalizeString));
|
|
1129
|
+
return `[${serializedEntries.join(",")}]`;
|
|
1130
|
+
}
|
|
1131
|
+
const record = value;
|
|
1132
|
+
if (normalizeString === preserveString) {
|
|
1133
|
+
const fields = Object.keys(record).sort();
|
|
1134
|
+
let fieldIndex = 0;
|
|
1135
|
+
for (const key of fields) fields[fieldIndex++] = `${JSON.stringify(key)}:${stringifyStableValue(record[key], stack, normalizeString)}`;
|
|
1136
|
+
return `{${fields.join(",")}}`;
|
|
1137
|
+
}
|
|
1138
|
+
const entries = Object.keys(record).map((key) => ({
|
|
1139
|
+
key,
|
|
1140
|
+
normalizedKey: normalizeString(key)
|
|
1141
|
+
})).sort((left, right) => {
|
|
1142
|
+
return compareStableStrings(left.normalizedKey, right.normalizedKey) || compareStableStrings(left.key, right.key);
|
|
1143
|
+
});
|
|
1144
|
+
const serializedFields = [];
|
|
1145
|
+
for (const { key, normalizedKey } of entries) serializedFields.push(`${JSON.stringify(normalizedKey)}:${stringifyStableValue(record[key], stack, normalizeString)}`);
|
|
1146
|
+
return `{${serializedFields.join(",")}}`;
|
|
1147
|
+
}
|
|
1148
|
+
function encodeBase64(value) {
|
|
1149
|
+
let binary = "";
|
|
1150
|
+
for (const byte of value) binary += String.fromCharCode(byte);
|
|
1151
|
+
return btoa(binary);
|
|
1152
|
+
}
|
|
1153
|
+
function compareStableStrings(left, right) {
|
|
1154
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
1155
|
+
}
|
|
1156
|
+
//#endregion
|
|
1035
1157
|
//#region packages/gateway-client/src/session-projection-message-identity.ts
|
|
1036
1158
|
function readSessionProjectionString(value) {
|
|
1037
1159
|
return typeof value === "string" ? value.trim() || null : null;
|
|
@@ -1057,15 +1179,18 @@ function readSessionMessageIdentity(message, envelope) {
|
|
|
1057
1179
|
const importedFrom = readSessionProjectionString(metadata?.importedFrom);
|
|
1058
1180
|
const cliSessionId = readSessionProjectionString(metadata?.cliSessionId);
|
|
1059
1181
|
const externalId = readSessionProjectionString(metadata?.externalId);
|
|
1182
|
+
const position = asNullableRecord(metadata?.transcriptPosition);
|
|
1183
|
+
const positionSource = readSessionProjectionString(position?.source);
|
|
1184
|
+
const isImported = !(positionSource !== null && positionSource.length <= 128 && typeof position?.rawSeq === "number" && Number.isSafeInteger(position.rawSeq) && position.rawSeq >= 0) && Boolean(importedFrom || cliSessionId || externalId);
|
|
1060
1185
|
const idempotencyKey = readSessionProjectionString(metadata?.idempotencyKey) ?? readSessionProjectionString(record.idempotencyKey) ?? readSessionProjectionString(envelope?.idempotencyKey) ?? readSessionProjectionString(envelope?.clientRunId);
|
|
1061
1186
|
const persistedRunId = normalizeSessionProjectionRunId(idempotencyKey);
|
|
1062
1187
|
const envelopeRunId = normalizeSessionProjectionRunId(envelope?.runId);
|
|
1063
1188
|
const metadataRunId = normalizeSessionProjectionRunId(metadata?.runId);
|
|
1189
|
+
const fallbackRunId = normalizeSessionProjectionRunId(asNullableRecord(record.openclawStreamFallback)?.runId);
|
|
1064
1190
|
const mirroredMessage = readSessionProjectionString(metadata?.mirrorOrigin) !== null;
|
|
1065
1191
|
const isCliAssistant = role === "assistant" && readSessionProjectionString(record.api)?.toLowerCase() === "cli";
|
|
1066
1192
|
const canonicalPersistedRunId = isCliAssistant && persistedRunId?.startsWith("cli-assistant:") ? readSessionProjectionString(persistedRunId.slice(14)) : persistedRunId;
|
|
1067
|
-
const
|
|
1068
|
-
const runId = role === "assistant" ? metadataRunId ?? envelopeRunId ?? (isCliAssistant || !mirroredMessage ? canonicalPersistedRunId : null) ?? optimisticRunId : metadataRunId ?? canonicalPersistedRunId ?? envelopeRunId;
|
|
1193
|
+
const runId = role === "assistant" ? metadataRunId ?? envelopeRunId ?? fallbackRunId ?? (isCliAssistant || !mirroredMessage ? canonicalPersistedRunId : null) : metadataRunId ?? canonicalPersistedRunId ?? envelopeRunId;
|
|
1069
1194
|
return {
|
|
1070
1195
|
role,
|
|
1071
1196
|
id: readSessionProjectionString(metadata?.id) ?? readSessionProjectionString(envelope?.messageId),
|
|
@@ -1073,8 +1198,8 @@ function readSessionMessageIdentity(message, envelope) {
|
|
|
1073
1198
|
idempotencyKey,
|
|
1074
1199
|
sendId: role === "user" ? persistedRunId ?? runId : null,
|
|
1075
1200
|
runId,
|
|
1076
|
-
isImported
|
|
1077
|
-
externalSource: importedFrom && cliSessionId && externalId ? JSON.stringify([
|
|
1201
|
+
isImported,
|
|
1202
|
+
externalSource: isImported && importedFrom && cliSessionId && externalId ? JSON.stringify([
|
|
1078
1203
|
importedFrom,
|
|
1079
1204
|
cliSessionId,
|
|
1080
1205
|
externalId
|
|
@@ -1087,11 +1212,51 @@ function readAssistantStreamSegmentIdentity(message) {
|
|
|
1087
1212
|
if (readSessionProjectionString(record?.role)?.toLowerCase() !== "assistant") return;
|
|
1088
1213
|
const fallback = asNullableRecord(record?.openclawStreamFallback);
|
|
1089
1214
|
const itemId = readSessionProjectionString(fallback?.itemId);
|
|
1215
|
+
if (!itemId) return;
|
|
1090
1216
|
const runId = readSessionMessageIdentity(message)?.runId ?? readSessionProjectionString(record?.runId) ?? readSessionProjectionString(fallback?.runId);
|
|
1091
|
-
return
|
|
1217
|
+
return {
|
|
1092
1218
|
itemId,
|
|
1093
1219
|
...runId ? { runId } : {}
|
|
1094
|
-
}
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
/** A saved occurrence can enrich its live projection, never another durable row. */
|
|
1223
|
+
function sameAssistantPersistenceReceipt(left, right) {
|
|
1224
|
+
return Boolean(left?.role === "assistant" && right?.role === "assistant" && !left.isImported && !right.isImported && left.idempotencyKey && left.idempotencyKey === right.idempotencyKey && (!left.id && left.sequence === null || !right.id && right.sequence === null));
|
|
1225
|
+
}
|
|
1226
|
+
/** Local turns have no durable transcript metadata beyond their own optional send key. */
|
|
1227
|
+
function isLocallyOptimisticSessionMessage(message) {
|
|
1228
|
+
const record = asNullableRecord(message);
|
|
1229
|
+
const role = readSessionProjectionString(record?.role)?.toLowerCase();
|
|
1230
|
+
if (role !== "user" && role !== "assistant") return false;
|
|
1231
|
+
if (asNullableRecord(record?.openclawStreamFallback)) return false;
|
|
1232
|
+
const metadata = asNullableRecord(record?.["__openclaw"]);
|
|
1233
|
+
return !metadata || Object.keys(metadata).every((key) => key === "idempotencyKey");
|
|
1234
|
+
}
|
|
1235
|
+
function sameTranscriptIdentity(left, right) {
|
|
1236
|
+
if (!left || !right || left.role !== right.role) return false;
|
|
1237
|
+
if (left.isImported || right.isImported) {
|
|
1238
|
+
if (!left.isImported || !right.isImported) return false;
|
|
1239
|
+
if (left.externalSource || right.externalSource) return Boolean(left.externalSource && left.externalSource === right.externalSource);
|
|
1240
|
+
return left.sequence !== null && right.sequence !== null && left.sequence === right.sequence;
|
|
1241
|
+
}
|
|
1242
|
+
if (left.id || right.id) return Boolean(left.id && right.id && left.id === right.id);
|
|
1243
|
+
return left.sequence !== null && right.sequence !== null && left.sequence === right.sequence;
|
|
1244
|
+
}
|
|
1245
|
+
/** Normalize a message into its live, durable, or pending projection entry. */
|
|
1246
|
+
function createSessionProjectionEntry(message, options) {
|
|
1247
|
+
const identity = readSessionMessageIdentity(message, options?.envelope);
|
|
1248
|
+
const fallback = asNullableRecord(asNullableRecord(message)?.openclawStreamFallback);
|
|
1249
|
+
const provisionalFallback = Boolean(fallback && identity?.role === "assistant" && !identity.id && identity.sequence === null);
|
|
1250
|
+
const inferredPendingRunId = options?.live !== true && isLocallyOptimisticSessionMessage(message) ? identity?.runId : null;
|
|
1251
|
+
const pendingRunId = normalizeSessionProjectionRunId(options?.pendingRunId ?? inferredPendingRunId);
|
|
1252
|
+
return {
|
|
1253
|
+
message,
|
|
1254
|
+
identity,
|
|
1255
|
+
afterSequence: options?.envelope?.afterSequence !== void 0 ? options.envelope.afterSequence : provisionalFallback && typeof fallback?.afterSequence === "number" ? fallback.afterSequence : void 0,
|
|
1256
|
+
live: options?.live === true || provisionalFallback,
|
|
1257
|
+
pending: pendingRunId !== null,
|
|
1258
|
+
pendingRunId
|
|
1259
|
+
};
|
|
1095
1260
|
}
|
|
1096
1261
|
//#endregion
|
|
1097
1262
|
//#region packages/gateway-client/src/session-projection-message-content.ts
|
|
@@ -1117,6 +1282,11 @@ function readSessionMessageDisplayContent(message) {
|
|
|
1117
1282
|
usesFallbackText: fallback !== null
|
|
1118
1283
|
};
|
|
1119
1284
|
}
|
|
1285
|
+
/** Check whether a projected message has text or another displayable block. */
|
|
1286
|
+
function hasDisplayableSessionMessage(message) {
|
|
1287
|
+
const { text, hasNonText } = readSessionMessageDisplayContent(message);
|
|
1288
|
+
return Boolean(text) || hasNonText;
|
|
1289
|
+
}
|
|
1120
1290
|
function normalizeChatErrorComparisonText(text) {
|
|
1121
1291
|
return text.trim().replace(/^⚠️\s*/u, "").replace(/^Error:\s*/iu, "").replace(/\s+/gu, " ").trim();
|
|
1122
1292
|
}
|
|
@@ -1129,6 +1299,153 @@ function isSessionProjectionErrorMessage(message, errorMessage) {
|
|
|
1129
1299
|
return !hasNonText && (!normalizedText || normalizedText === "[assistant turn failed before producing content]" || normalizedText === GATEWAY_ASSISTANT_ERROR_FALLBACK_TEXT || normalizedText === normalizeChatErrorComparisonText(errorMessage ?? ""));
|
|
1130
1300
|
}
|
|
1131
1301
|
//#endregion
|
|
1302
|
+
//#region packages/gateway-client/src/session-projection-final-identity.ts
|
|
1303
|
+
/** 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
|
+
*/
|
|
1310
|
+
function isToolUsePersistedFinalRow(message) {
|
|
1311
|
+
return asNullableRecord(message)?.["stopReason"] === "toolUse" && !isSessionProjectionToolContinuation(message);
|
|
1312
|
+
}
|
|
1313
|
+
/** Tool-bearing assistant rows are continuations even without a tool stop reason. */
|
|
1314
|
+
function isSessionProjectionToolContinuation(message) {
|
|
1315
|
+
const record = asNullableRecord(message);
|
|
1316
|
+
if (Array.isArray(record?.content) && record.content.some((block) => {
|
|
1317
|
+
const type = asNullableRecord(block)?.type;
|
|
1318
|
+
return type === "toolCall" || type === "toolUse" || type === "functionCall";
|
|
1319
|
+
})) return true;
|
|
1320
|
+
return record?.stopReason === "toolUse" && !hasDisplayableSessionMessage(message);
|
|
1321
|
+
}
|
|
1322
|
+
function readPersistedFinalIdentity(message) {
|
|
1323
|
+
const identity = readSessionMessageIdentity(message);
|
|
1324
|
+
if (identity?.externalSource) return `import:${identity.role}:${identity.externalSource}`;
|
|
1325
|
+
if (identity?.id && !identity.isImported) return `id:${identity.role}:${identity.id}`;
|
|
1326
|
+
if (identity?.sequence !== null && identity?.sequence !== void 0) return `seq:${identity.role}:${identity.sequence}`;
|
|
1327
|
+
if (identity?.role === "assistant" && !identity.isImported && identity.idempotencyKey) return `key:assistant:${identity.idempotencyKey}`;
|
|
1328
|
+
return null;
|
|
1329
|
+
}
|
|
1330
|
+
function hasCompatiblePersistedFinalIdentity(currentMessage, incomingMessage) {
|
|
1331
|
+
const current = readSessionMessageIdentity(currentMessage);
|
|
1332
|
+
const incoming = readSessionMessageIdentity(incomingMessage);
|
|
1333
|
+
if (!current || !incoming || current.role !== incoming.role) return false;
|
|
1334
|
+
if (current.isImported || incoming.isImported) {
|
|
1335
|
+
if (!current.isImported || !incoming.isImported) return false;
|
|
1336
|
+
if (current.externalSource && incoming.externalSource) return current.externalSource === incoming.externalSource;
|
|
1337
|
+
return current.sequence !== null && incoming.sequence !== null && current.sequence === incoming.sequence;
|
|
1338
|
+
}
|
|
1339
|
+
if (sameAssistantPersistenceReceipt(current, incoming)) return true;
|
|
1340
|
+
if (current.id && incoming.id) return current.id === incoming.id;
|
|
1341
|
+
return current.sequence !== null && incoming.sequence !== null && current.sequence === incoming.sequence;
|
|
1342
|
+
}
|
|
1343
|
+
function readFinalContentIdentity(message) {
|
|
1344
|
+
const display = readSessionMessageDisplayContent(message);
|
|
1345
|
+
if (!display.text && !display.hasNonText) return null;
|
|
1346
|
+
const identity = readSessionMessageIdentity(message);
|
|
1347
|
+
const record = asNullableRecord(message);
|
|
1348
|
+
const metadata = asNullableRecord(record?.["__openclaw"]);
|
|
1349
|
+
try {
|
|
1350
|
+
return `content:${stableStringify([
|
|
1351
|
+
identity?.role ?? "assistant",
|
|
1352
|
+
display.text,
|
|
1353
|
+
display.hasNonText ? record?.content ?? null : null,
|
|
1354
|
+
metadata?.media ?? null,
|
|
1355
|
+
identity?.isImported ? [
|
|
1356
|
+
metadata?.importedFrom ?? null,
|
|
1357
|
+
metadata?.cliSessionId ?? null,
|
|
1358
|
+
metadata?.externalId ?? null
|
|
1359
|
+
] : null
|
|
1360
|
+
])}`;
|
|
1361
|
+
} catch {
|
|
1362
|
+
return null;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
function hasTerminalStopReason(message) {
|
|
1366
|
+
const stopReason = asNullableRecord(message)?.stopReason;
|
|
1367
|
+
return stopReason === "stop" || stopReason === "length" || stopReason === "error" || stopReason === "aborted" || stopReason === "end_turn";
|
|
1368
|
+
}
|
|
1369
|
+
/** Read stable persisted identity first, falling back to canonical display content. */
|
|
1370
|
+
function readSessionProjectionFinalMessageIdentity(message) {
|
|
1371
|
+
if (!hasDisplayableSessionMessage(message)) return null;
|
|
1372
|
+
return readPersistedFinalIdentity(message) ?? readFinalContentIdentity(message);
|
|
1373
|
+
}
|
|
1374
|
+
/** Check whether a displayable terminal may recover a prior empty terminal. */
|
|
1375
|
+
function canRecoverSessionProjectionFinal(currentMessage, incomingMessage) {
|
|
1376
|
+
if (hasDisplayableSessionMessage(currentMessage)) return false;
|
|
1377
|
+
return readPersistedFinalIdentity(currentMessage) === null || hasCompatiblePersistedFinalIdentity(currentMessage, incomingMessage);
|
|
1378
|
+
}
|
|
1379
|
+
/** Check whether a run has already accepted the same terminal reply. */
|
|
1380
|
+
function hasSessionProjectionAcceptedFinal(run, message) {
|
|
1381
|
+
const identity = readSessionProjectionFinalMessageIdentity(message);
|
|
1382
|
+
return Boolean(identity && run && (run.acceptedFinalMessageIdentities?.includes(identity) || readSessionProjectionFinalMessageIdentity(run.message) === identity));
|
|
1383
|
+
}
|
|
1384
|
+
/** Match an unsequenced live terminal to exactly one durable same-run terminal row. */
|
|
1385
|
+
function findUniqueSnapshotTerminalMatch(current, matches, run, snapshot) {
|
|
1386
|
+
if (!current.live || current.identity?.role !== "assistant" || current.identity.id || current.identity.sequence !== null || !run || run.status === "streaming" || matches.length === 0) return null;
|
|
1387
|
+
const terminalContent = readFinalContentIdentity(current.message);
|
|
1388
|
+
if (!terminalContent || readFinalContentIdentity(run.message) !== terminalContent) return null;
|
|
1389
|
+
let snapshotIndexes;
|
|
1390
|
+
let firstUserIndex = -1;
|
|
1391
|
+
let lastAssistantIndex = -1;
|
|
1392
|
+
const ensureRunIndexes = () => {
|
|
1393
|
+
if (snapshotIndexes) return;
|
|
1394
|
+
const runId = current.identity?.runId;
|
|
1395
|
+
const indexes = /* @__PURE__ */ new Map();
|
|
1396
|
+
if (runId) snapshot.forEach((candidate, index) => {
|
|
1397
|
+
if (candidate.identity?.runId === runId) {
|
|
1398
|
+
if (!indexes.has(candidate)) indexes.set(candidate, index);
|
|
1399
|
+
if (candidate.identity.role === "user" && firstUserIndex < 0) firstUserIndex = index;
|
|
1400
|
+
else if (candidate.identity.role === "assistant") lastAssistantIndex = index;
|
|
1401
|
+
}
|
|
1402
|
+
});
|
|
1403
|
+
snapshotIndexes = indexes;
|
|
1404
|
+
};
|
|
1405
|
+
const hasCompletedRunSnapshotContext = (entry) => {
|
|
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) => {
|
|
1413
|
+
const runId = current.identity?.runId;
|
|
1414
|
+
if (!runId || entry.identity?.runId !== runId) return false;
|
|
1415
|
+
ensureRunIndexes();
|
|
1416
|
+
const entryIndex = snapshotIndexes?.get(entry);
|
|
1417
|
+
return entryIndex !== void 0 && lastAssistantIndex >= 0 && entryIndex >= lastAssistantIndex;
|
|
1418
|
+
};
|
|
1419
|
+
const durableTerminalMatches = matches.filter((entry) => {
|
|
1420
|
+
return (asNullableRecord(asNullableRecord(entry.message)?.["__openclaw"])?.runTerminal === true || entry.identity?.runId === current.identity?.runId && (hasTerminalStopReason(entry.message) || asNullableRecord(entry.message)?.["stopReason"] === "toolUse" && !isSessionProjectionToolContinuation(entry.message) && isLastSameRunAssistantRow(entry)) || hasCompletedRunSnapshotContext(entry)) && readFinalContentIdentity(entry.message) === terminalContent;
|
|
1421
|
+
});
|
|
1422
|
+
const entry = durableTerminalMatches.length === 1 ? durableTerminalMatches[0] : void 0;
|
|
1423
|
+
if (!entry) return null;
|
|
1424
|
+
return {
|
|
1425
|
+
entry,
|
|
1426
|
+
inferred: asNullableRecord(asNullableRecord(entry.message)?.["__openclaw"])?.runTerminal !== true && !hasTerminalStopReason(entry.message)
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
/** Check whether ordinary single-match promotion needs terminal-content verification. */
|
|
1430
|
+
function isUnsequencedLiveTerminal(current, run) {
|
|
1431
|
+
return Boolean(current.live && current.identity?.role === "assistant" && !current.identity.id && current.identity.sequence === null && run && run.status !== "streaming" && readFinalContentIdentity(current.message) === readFinalContentIdentity(run.message));
|
|
1432
|
+
}
|
|
1433
|
+
//#endregion
|
|
1434
|
+
//#region packages/gateway-client/src/session-projection-run-retention.ts
|
|
1435
|
+
/** Bounded retention for completed and active session projection runs. */
|
|
1436
|
+
const MAX_TRACKED_SESSION_RUNS = 200;
|
|
1437
|
+
const RETAINED_SESSION_RUNS = 150;
|
|
1438
|
+
/** Retain every active run and the newest completed runs within the projection bound. */
|
|
1439
|
+
function retainSessionProjectionRuns(runs) {
|
|
1440
|
+
const entries = Object.entries(runs);
|
|
1441
|
+
if (entries.length <= MAX_TRACKED_SESSION_RUNS) return runs;
|
|
1442
|
+
const active = entries.filter(([, run]) => run.status === "streaming");
|
|
1443
|
+
const terminal = entries.filter(([, run]) => run.status !== "streaming");
|
|
1444
|
+
const terminalLimit = Math.max(0, RETAINED_SESSION_RUNS - active.length);
|
|
1445
|
+
const retainedTerminal = terminalLimit > 0 ? terminal.slice(-terminalLimit) : [];
|
|
1446
|
+
return Object.fromEntries([...active, ...retainedTerminal]);
|
|
1447
|
+
}
|
|
1448
|
+
//#endregion
|
|
1132
1449
|
//#region packages/gateway-client/src/session-projection-run-event.ts
|
|
1133
1450
|
function readNonemptyString(value) {
|
|
1134
1451
|
return typeof value === "string" ? value.trim() || null : null;
|
|
@@ -1172,8 +1489,6 @@ function reduceSessionProjectionRunEvent(projection, event, scope = {}) {
|
|
|
1172
1489
|
//#endregion
|
|
1173
1490
|
//#region packages/gateway-client/src/session-projection.ts
|
|
1174
1491
|
/** Browser-safe identity and replay rules shared by Gateway conversation clients. */
|
|
1175
|
-
const MAX_TRACKED_SESSION_RUNS = 200;
|
|
1176
|
-
const RETAINED_SESSION_RUNS = 150;
|
|
1177
1492
|
const SESSION_PROJECTION_SCOPE_KEYS = [
|
|
1178
1493
|
"sessionKey",
|
|
1179
1494
|
"sessionId",
|
|
@@ -1181,35 +1496,15 @@ const SESSION_PROJECTION_SCOPE_KEYS = [
|
|
|
1181
1496
|
"lifecycleRevision",
|
|
1182
1497
|
"activeLeafEntryId"
|
|
1183
1498
|
];
|
|
1184
|
-
/** Local turns have no durable transcript metadata beyond their own optional send key. */
|
|
1185
|
-
function isLocallyOptimisticSessionMessage(message) {
|
|
1186
|
-
const identity = readSessionMessageIdentity(message);
|
|
1187
|
-
if (!identity || identity.role !== "user" && identity.role !== "assistant") return false;
|
|
1188
|
-
const metadata = asNullableRecord(asNullableRecord(message)?.["__openclaw"]);
|
|
1189
|
-
return !metadata || Object.keys(metadata).every((key) => key === "idempotencyKey");
|
|
1190
|
-
}
|
|
1191
|
-
function createEntry(message, options) {
|
|
1192
|
-
const identity = readSessionMessageIdentity(message, options?.envelope);
|
|
1193
|
-
const inferredPendingRunId = options?.live !== true && isLocallyOptimisticSessionMessage(message) ? identity?.runId : null;
|
|
1194
|
-
const pendingRunId = normalizeSessionProjectionRunId(options?.pendingRunId ?? inferredPendingRunId);
|
|
1195
|
-
return {
|
|
1196
|
-
message,
|
|
1197
|
-
identity,
|
|
1198
|
-
afterSequence: options?.envelope?.afterSequence,
|
|
1199
|
-
live: options?.live === true,
|
|
1200
|
-
pending: pendingRunId !== null,
|
|
1201
|
-
pendingRunId
|
|
1202
|
-
};
|
|
1203
|
-
}
|
|
1204
1499
|
function createProjectionEntries(messages) {
|
|
1205
1500
|
let pendingUserRunId = null;
|
|
1206
1501
|
return messages.map((message) => {
|
|
1207
|
-
const entry =
|
|
1502
|
+
const entry = createSessionProjectionEntry(message);
|
|
1208
1503
|
if (entry.identity?.role === "user") {
|
|
1209
1504
|
pendingUserRunId = entry.pending ? entry.pendingRunId : null;
|
|
1210
1505
|
return entry;
|
|
1211
1506
|
}
|
|
1212
|
-
if (pendingUserRunId && entry.identity?.role === "assistant" && !entry.pending && isLocallyOptimisticSessionMessage(message)) return
|
|
1507
|
+
if (pendingUserRunId && entry.identity?.role === "assistant" && !entry.pending && isLocallyOptimisticSessionMessage(message)) return createSessionProjectionEntry(message, { pendingRunId: pendingUserRunId });
|
|
1213
1508
|
if (!isLocallyOptimisticSessionMessage(message)) pendingUserRunId = null;
|
|
1214
1509
|
return entry;
|
|
1215
1510
|
});
|
|
@@ -1232,26 +1527,21 @@ function readEventScope(event) {
|
|
|
1232
1527
|
for (const key of SESSION_PROJECTION_SCOPE_KEYS) if (event[key] !== void 0) Object.assign(scope, { [key]: event[key] });
|
|
1233
1528
|
return scope;
|
|
1234
1529
|
}
|
|
1235
|
-
function sameTranscriptIdentity(left, right) {
|
|
1236
|
-
if (!left || !right || left.role !== right.role) return false;
|
|
1237
|
-
if (left.isImported || right.isImported) {
|
|
1238
|
-
if (!left.isImported || !right.isImported) return false;
|
|
1239
|
-
if (left.externalSource || right.externalSource) return Boolean(left.externalSource && left.externalSource === right.externalSource);
|
|
1240
|
-
return left.sequence !== null && right.sequence !== null && left.sequence === right.sequence;
|
|
1241
|
-
}
|
|
1242
|
-
if (left.id || right.id) return Boolean(left.id && right.id && left.id === right.id);
|
|
1243
|
-
return left.sequence !== null && right.sequence !== null && left.sequence === right.sequence;
|
|
1244
|
-
}
|
|
1245
1530
|
function entryMatches(left, right, allowSnapshotPromotion = false) {
|
|
1531
|
+
const leftSegment = readAssistantStreamSegmentIdentity(left.message);
|
|
1532
|
+
const rightSegment = readAssistantStreamSegmentIdentity(right.message);
|
|
1533
|
+
if (leftSegment?.itemId !== rightSegment?.itemId) return false;
|
|
1246
1534
|
if (sameTranscriptIdentity(left.identity, right.identity)) return true;
|
|
1535
|
+
if (sameAssistantPersistenceReceipt(left.identity, right.identity)) return true;
|
|
1536
|
+
if (left.identity?.role === "assistant" && right.identity?.role === "assistant" && left.identity.idempotencyKey && right.identity.idempotencyKey && left.identity.idempotencyKey !== right.identity.idempotencyKey) return false;
|
|
1247
1537
|
const durableEntry = left.identity?.id ? left : right.identity?.id ? right : null;
|
|
1248
1538
|
const provisionalEntry = durableEntry === left ? right : durableEntry === right ? left : null;
|
|
1249
1539
|
const durableMetadata = asNullableRecord(asNullableRecord(durableEntry?.message)?.["__openclaw"]);
|
|
1250
1540
|
if (durableEntry?.identity?.role === "assistant" && provisionalEntry?.identity?.role === "assistant" && !durableEntry.identity.isImported && !provisionalEntry.identity.isImported && !provisionalEntry.identity.id) {
|
|
1251
|
-
const durableSegment =
|
|
1252
|
-
const provisionalSegment =
|
|
1541
|
+
const durableSegment = durableEntry === left ? leftSegment : rightSegment;
|
|
1542
|
+
const provisionalSegment = durableEntry === left ? rightSegment : leftSegment;
|
|
1253
1543
|
if (provisionalEntry.identity.sequence === null && durableSegment?.runId && durableSegment.runId === provisionalSegment?.runId && durableSegment.itemId === provisionalSegment.itemId) return true;
|
|
1254
|
-
if (provisionalEntry.live && provisionalEntry.identity.sequence === null && (provisionalEntry.afterSequence === void 0 || provisionalEntry.afterSequence !== null && durableEntry.identity.sequence !== null && durableEntry.identity.sequence > provisionalEntry.afterSequence) && durableEntry.identity.runId && durableEntry.identity.runId === provisionalEntry.identity.runId && (readSessionProjectionString(durableMetadata?.mirrorOrigin) === null || durableMetadata?.runTerminal === true)) return true;
|
|
1544
|
+
if (provisionalEntry.live && !durableSegment && !isSessionProjectionToolContinuation(durableEntry.message) && (!isToolUsePersistedFinalRow(durableEntry.message) || readFinalContentIdentity(durableEntry.message) === readFinalContentIdentity(provisionalEntry.message)) && provisionalEntry.identity.sequence === null && (provisionalEntry.afterSequence === void 0 || provisionalEntry.afterSequence !== null && durableEntry.identity.sequence !== null && durableEntry.identity.sequence > provisionalEntry.afterSequence) && durableEntry.identity.runId && durableEntry.identity.runId === provisionalEntry.identity.runId && (readSessionProjectionString(durableMetadata?.mirrorOrigin) === null || durableMetadata?.runTerminal === true)) return true;
|
|
1255
1545
|
}
|
|
1256
1546
|
const persisted = left.identity;
|
|
1257
1547
|
const observed = right.identity;
|
|
@@ -1299,40 +1589,94 @@ function insertEntry(entries, incoming, runs) {
|
|
|
1299
1589
|
...entries.slice(nextIndex)
|
|
1300
1590
|
];
|
|
1301
1591
|
}
|
|
1302
|
-
function projectLiveSessionMessage(
|
|
1592
|
+
function projectLiveSessionMessage(initialState, message, envelope, scope = {}) {
|
|
1593
|
+
let state = initialState;
|
|
1303
1594
|
if (!scopesMatch(state.scope, scope)) return state;
|
|
1304
|
-
const incoming =
|
|
1595
|
+
const incoming = createSessionProjectionEntry(message, {
|
|
1305
1596
|
envelope,
|
|
1306
1597
|
live: true
|
|
1307
1598
|
});
|
|
1308
1599
|
if (!incoming.identity) return state;
|
|
1309
1600
|
const matches = state.entries.filter((entry) => entryMatches(entry, incoming));
|
|
1310
1601
|
const existing = matches.find((entry) => sameTranscriptIdentity(entry.identity, incoming.identity)) ?? (matches.length === 1 ? matches[0] : void 0);
|
|
1602
|
+
if (existing && !sameTranscriptIdentity(existing.identity, incoming.identity) && !sameAssistantPersistenceReceipt(existing.identity, incoming.identity)) {
|
|
1603
|
+
const durable = existing.identity?.id ? existing : incoming.identity.id ? incoming : null;
|
|
1604
|
+
const provisional = durable === existing ? incoming : existing;
|
|
1605
|
+
if (durable && isToolUsePersistedFinalRow(durable.message)) {
|
|
1606
|
+
const history = state.entries.filter((entry) => entry !== provisional);
|
|
1607
|
+
const snapshot = durable === incoming ? insertEntry(history, durable) : history;
|
|
1608
|
+
const runId = provisional.identity?.runId;
|
|
1609
|
+
const run = runId ? state.runs[runId] : void 0;
|
|
1610
|
+
const terminalMatch = findUniqueSnapshotTerminalMatch(provisional, [durable], run, snapshot);
|
|
1611
|
+
if (!terminalMatch) return withEntries(state, insertEntry(state.entries, incoming, state.runs));
|
|
1612
|
+
if (terminalMatch.inferred && durable.identity && runId && run) {
|
|
1613
|
+
const inferredSnapshotTerminal = {
|
|
1614
|
+
entry: provisional,
|
|
1615
|
+
matchedIdentity: durable.identity
|
|
1616
|
+
};
|
|
1617
|
+
state = {
|
|
1618
|
+
...state,
|
|
1619
|
+
runs: {
|
|
1620
|
+
...state.runs,
|
|
1621
|
+
[runId]: {
|
|
1622
|
+
...run,
|
|
1623
|
+
inferredSnapshotTerminal
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1311
1630
|
if (!existing) return withEntries(state, insertEntry(state.entries, incoming, state.runs));
|
|
1312
1631
|
const existingIndex = state.entries.indexOf(existing);
|
|
1313
1632
|
if (existing.message === message && existing.live && !existing.pending) return state;
|
|
1314
1633
|
if (!existing.pending && existing.identity?.id && !incoming.identity.id) return state;
|
|
1315
1634
|
if (incoming.identity.sequence !== null && (existing.pending || existing.identity?.sequence === null)) {
|
|
1316
1635
|
const sequence = incoming.identity.sequence;
|
|
1317
|
-
|
|
1636
|
+
const violatesOrder = state.entries.some(({ identity }, index) => identity?.sequence != null && (index < existingIndex ? identity.sequence > sequence : identity.sequence < sequence));
|
|
1637
|
+
return withEntries(state, violatesOrder ? insertEntry(state.entries.filter((_, index) => index !== existingIndex), incoming, state.runs) : state.entries.toSpliced(existingIndex, 1, incoming));
|
|
1318
1638
|
}
|
|
1319
|
-
return withEntries(state,
|
|
1320
|
-
...state.entries.slice(0, existingIndex),
|
|
1321
|
-
incoming,
|
|
1322
|
-
...state.entries.slice(existingIndex + 1)
|
|
1323
|
-
]);
|
|
1639
|
+
return withEntries(state, state.entries.toSpliced(existingIndex, 1, incoming));
|
|
1324
1640
|
}
|
|
1325
1641
|
/** Only observed live events and this client's pending turns may survive an older snapshot. */
|
|
1326
1642
|
function reconcileSessionProjectionSnapshot(state, messages, scope = {}, options = {}) {
|
|
1327
1643
|
const visibleMessages = options.shouldIncludeMessage ? messages.filter(options.shouldIncludeMessage) : messages;
|
|
1328
1644
|
if (!scopesMatch(state.scope, scope)) return createSessionProjection(scope, visibleMessages);
|
|
1329
1645
|
let entries = createProjectionEntries(visibleMessages);
|
|
1646
|
+
const runs = { ...state.runs };
|
|
1330
1647
|
for (const current of state.entries) {
|
|
1331
|
-
if (!current.live && !current.pending || options.shouldIncludeMessage?.(current.message) === false
|
|
1332
|
-
|
|
1648
|
+
if (!current.live && !current.pending || options.shouldIncludeMessage?.(current.message) === false) continue;
|
|
1649
|
+
const matches = entries.filter((entry) => entryMatches(entry, current, true));
|
|
1650
|
+
const uniqueMatch = matches.length === 1 ? matches[0] : void 0;
|
|
1651
|
+
const run = current.identity?.runId ? runs[current.identity.runId] : void 0;
|
|
1652
|
+
const terminalMatch = findUniqueSnapshotTerminalMatch(current, matches, run, entries);
|
|
1653
|
+
if (uniqueMatch && (sameAssistantPersistenceReceipt(uniqueMatch.identity, current.identity) || !isUnsequencedLiveTerminal(current, run)) || terminalMatch) {
|
|
1654
|
+
if (terminalMatch?.inferred && terminalMatch.entry.identity && current.identity?.runId && run) runs[current.identity.runId] = {
|
|
1655
|
+
...run,
|
|
1656
|
+
inferredSnapshotTerminal: {
|
|
1657
|
+
entry: current,
|
|
1658
|
+
matchedIdentity: terminalMatch.entry.identity
|
|
1659
|
+
}
|
|
1660
|
+
};
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
entries = insertEntry(entries, current, runs);
|
|
1664
|
+
}
|
|
1665
|
+
for (const [runId, run] of Object.entries(runs)) {
|
|
1666
|
+
const inferred = run.inferredSnapshotTerminal;
|
|
1667
|
+
if (!inferred) continue;
|
|
1668
|
+
const candidateRemains = entries.some((entry) => sameTranscriptIdentity(entry.identity, inferred.matchedIdentity));
|
|
1669
|
+
const visible = options.shouldIncludeMessage?.(inferred.entry.message) !== false;
|
|
1670
|
+
const matches = entries.filter((entry) => entryMatches(entry, inferred.entry, true));
|
|
1671
|
+
const terminalMatch = findUniqueSnapshotTerminalMatch(inferred.entry, matches, run, entries);
|
|
1672
|
+
if (candidateRemains && visible && terminalMatch?.inferred) continue;
|
|
1673
|
+
if (candidateRemains && visible && !terminalMatch) entries = insertEntry(entries, inferred.entry, runs);
|
|
1674
|
+
const { inferredSnapshotTerminal: _inferred, ...settledRun } = run;
|
|
1675
|
+
runs[runId] = settledRun;
|
|
1333
1676
|
}
|
|
1334
1677
|
return {
|
|
1335
1678
|
...withEntries(state, entries),
|
|
1679
|
+
runs,
|
|
1336
1680
|
scope: {
|
|
1337
1681
|
...state.scope,
|
|
1338
1682
|
...scope
|
|
@@ -1340,49 +1684,6 @@ function reconcileSessionProjectionSnapshot(state, messages, scope = {}, options
|
|
|
1340
1684
|
hasTransportGap: false
|
|
1341
1685
|
};
|
|
1342
1686
|
}
|
|
1343
|
-
function hasDisplayableSessionMessage(message) {
|
|
1344
|
-
const { text, hasNonText } = readSessionMessageDisplayContent(message);
|
|
1345
|
-
return Boolean(text) || hasNonText;
|
|
1346
|
-
}
|
|
1347
|
-
function readSessionProjectionFinalMessageIdentity(message) {
|
|
1348
|
-
const display = readSessionMessageDisplayContent(message);
|
|
1349
|
-
if (!display.text && !display.hasNonText) return null;
|
|
1350
|
-
const identity = readSessionMessageIdentity(message);
|
|
1351
|
-
if (identity?.externalSource) return `import:${identity.role}:${identity.externalSource}`;
|
|
1352
|
-
if (identity?.id && !identity.isImported) return `id:${identity.role}:${identity.id}`;
|
|
1353
|
-
if (identity?.sequence !== null && identity?.sequence !== void 0) return `seq:${identity.role}:${identity.sequence}`;
|
|
1354
|
-
const record = asNullableRecord(message);
|
|
1355
|
-
const metadata = asNullableRecord(record?.["__openclaw"]);
|
|
1356
|
-
try {
|
|
1357
|
-
return `content:${JSON.stringify([
|
|
1358
|
-
identity?.role ?? "assistant",
|
|
1359
|
-
typeof message === "string" ? message : record?.content ?? null,
|
|
1360
|
-
metadata?.media ?? null,
|
|
1361
|
-
identity?.isImported ? [
|
|
1362
|
-
metadata?.importedFrom ?? null,
|
|
1363
|
-
metadata?.cliSessionId ?? null,
|
|
1364
|
-
metadata?.externalId ?? null
|
|
1365
|
-
] : null,
|
|
1366
|
-
...display.usesFallbackText ? [record?.text] : []
|
|
1367
|
-
])}`;
|
|
1368
|
-
} catch {
|
|
1369
|
-
return null;
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
/** Replayed finals are recognized against this run's bounded canonical terminal history. */
|
|
1373
|
-
function hasSessionProjectionAcceptedFinal(run, message) {
|
|
1374
|
-
const identity = readSessionProjectionFinalMessageIdentity(message);
|
|
1375
|
-
return Boolean(identity && run && (run.acceptedFinalMessageIdentities?.includes(identity) || readSessionProjectionFinalMessageIdentity(run.message) === identity));
|
|
1376
|
-
}
|
|
1377
|
-
function retainSessionProjectionRuns(runs) {
|
|
1378
|
-
const entries = Object.entries(runs);
|
|
1379
|
-
if (entries.length <= MAX_TRACKED_SESSION_RUNS) return runs;
|
|
1380
|
-
const active = entries.filter(([, run]) => run.status === "streaming");
|
|
1381
|
-
const terminal = entries.filter(([, run]) => run.status !== "streaming");
|
|
1382
|
-
const terminalLimit = Math.max(0, RETAINED_SESSION_RUNS - active.length);
|
|
1383
|
-
const retainedTerminal = terminalLimit > 0 ? terminal.slice(-terminalLimit) : [];
|
|
1384
|
-
return Object.fromEntries([...active, ...retainedTerminal]);
|
|
1385
|
-
}
|
|
1386
1687
|
function updateRun(state, incoming) {
|
|
1387
1688
|
const incomingErrorMessage = readSessionProjectionString(incoming.errorMessage);
|
|
1388
1689
|
const incomingSeq = typeof incoming.seq === "number" && Number.isSafeInteger(incoming.seq) && incoming.seq >= 0 ? incoming.seq : void 0;
|
|
@@ -1397,9 +1698,10 @@ function updateRun(state, incoming) {
|
|
|
1397
1698
|
if (current && current.status !== "streaming" && !resumesErrorProjection) {
|
|
1398
1699
|
const incomingFinalIdentity = readSessionProjectionFinalMessageIdentity(incoming.message);
|
|
1399
1700
|
const incomingIsFinal = incoming.status === "completed" || incoming.status === "yielded";
|
|
1400
|
-
const
|
|
1401
|
-
const
|
|
1402
|
-
const
|
|
1701
|
+
const currentHasDisplayableMessage = hasDisplayableSessionMessage(current.message);
|
|
1702
|
+
const canAcceptFinal = currentHasDisplayableMessage ? current.status === incoming.status || (current.acceptedFinalMessageIdentities?.length ?? 0) > 0 : canRecoverSessionProjectionFinal(current.message, incoming.message);
|
|
1703
|
+
const acceptFinal = incomingIsFinal && canAcceptFinal && incomingFinalIdentity !== null && !hasSessionProjectionAcceptedFinal(current, incoming.message);
|
|
1704
|
+
const recoverMessage = acceptFinal && !currentHasDisplayableMessage;
|
|
1403
1705
|
const recoverError = readSessionProjectionString(current.errorMessage) === null && incomingErrorMessage !== null;
|
|
1404
1706
|
const updateTerminalSequence = incoming.status !== "streaming" && (incomingSeq === void 0 ? current.seq !== void 0 : current.seq === void 0 || incomingSeq > current.seq);
|
|
1405
1707
|
if (!acceptFinal && !recoverError && !updateTerminalSequence) return state;
|
|
@@ -1458,7 +1760,7 @@ function reduceSessionProjection(state, event) {
|
|
|
1458
1760
|
case "messagePersisted": return projectLiveSessionMessage(state, event.message, event.envelope ?? event, scope);
|
|
1459
1761
|
case "sendPending": {
|
|
1460
1762
|
const pendingRunId = normalizeSessionProjectionRunId(event.idempotencyKey ?? event.runId);
|
|
1461
|
-
const incoming =
|
|
1763
|
+
const incoming = createSessionProjectionEntry(event.message, { pendingRunId });
|
|
1462
1764
|
if (!pendingRunId || !incoming.identity) return state;
|
|
1463
1765
|
const seed = state.entries.find((entry) => entry.message === event.message);
|
|
1464
1766
|
if (seed && !seed.pending && incoming.identity.id === null && !incoming.identity.isImported && incoming.identity.runId === pendingRunId) return withEntries(state, state.entries.map((entry) => entry === seed ? {
|
|
@@ -1727,4 +2029,4 @@ function releaseGatewaySessionMessageSubscription(subscription) {
|
|
|
1727
2029
|
return sessionMessageSubscriptionOwners.get(subscription)?.coordinator.release(subscription) ?? Promise.resolve();
|
|
1728
2030
|
}
|
|
1729
2031
|
//#endregion
|
|
1730
|
-
export {
|
|
2032
|
+
export { selectGatewayConnectAuth as A, isGatewayProtocolResponseError as C, GatewayBrowserDeviceAuthLifecycle as D, gatewayOriginScope as E, buildDeviceAuthPayload as M, buildDeviceAuthPayloadV3 as N, buildGatewayConnectAuth as O, normalizeDeviceMetadataForAuth as P, GatewayProtocolRequestTimeoutError as S, gatewayCredentialScope as T, readSessionMessageSequence as _, createSessionProjection as a, GatewayProtocolClient as b, reduceSessionProjection as c, readSessionProjectionFinalMessageIdentity as d, isSessionProjectionErrorMessage as f, readSessionMessageIdentity as g, readAssistantStreamSegmentIdentity as h, resetGatewaySessionMessageSubscriptionCoordinator as i, shouldRetryGatewayWithDeviceToken as j, resolveGatewayConnectScopes as k, reduceSessionProjectionRunEvent as l, normalizeSessionProjectionRunId as m, getGatewaySessionMessageSubscriptionCoordinator as n, projectLiveSessionMessage as o, isLocallyOptimisticSessionMessage as p, releaseGatewaySessionMessageSubscription as r, reconcileSessionProjectionSnapshot as s, GatewaySessionMessageSubscriptionCoordinator as t, hasSessionProjectionAcceptedFinal as u, isRecord as v, resolveModelCatalogConnect as w, GatewayProtocolRequestError as x, shouldPauseGatewayReconnect as y };
|