ocuclaw 1.3.2 → 1.3.4
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 +29 -1
- package/dist/config/runtime-config-session-title-model.test.js +0 -3
- package/dist/config/runtime-config.js +22 -33
- package/dist/domain/activity-status-adapter.js +0 -7
- package/dist/domain/activity-status-arbiter.js +3 -27
- package/dist/domain/activity-status-labels.js +8 -38
- package/dist/domain/code-span-regions.js +4 -24
- package/dist/domain/constant-time-equal.js +9 -0
- package/dist/domain/constant-time-equal.test.js +28 -0
- package/dist/domain/conversation-state.js +27 -138
- package/dist/domain/debug-bundle-cache.js +52 -0
- package/dist/domain/debug-bundle-format.js +60 -0
- package/dist/domain/debug-bundle-preview.js +123 -0
- package/dist/domain/debug-bundle-redaction.js +182 -0
- package/dist/domain/debug-bundle-save.js +11 -0
- package/dist/domain/debug-bundle-zip.js +15 -0
- package/dist/domain/debug-bundle.js +97 -0
- package/dist/domain/debug-store.js +6 -17
- package/dist/domain/debug-upload-preset.js +27 -0
- package/dist/domain/glasses-display-system-prompt.js +0 -5
- package/dist/domain/glasses-display-system-prompt.test.js +1 -1
- package/dist/domain/glasses-ui-content-summary.js +0 -6
- package/dist/domain/glasses-ui-system-prompt.test.js +1 -2
- package/dist/domain/message-emoji-allowlist.js +0 -7
- package/dist/domain/message-emoji-filter.js +3 -9
- package/dist/domain/neural-emoji-reactor-tag-config.js +3 -3
- package/dist/domain/prompt-channel-fragments.js +1 -10
- package/dist/domain/tagged-span-parser.js +3 -26
- package/dist/domain/tagged-span-strip.js +0 -7
- package/dist/even-ai/even-ai-endpoint.js +77 -24
- package/dist/even-ai/even-ai-run-waiter.js +0 -1
- package/dist/even-ai/even-ai-settings-store.js +11 -0
- package/dist/gateway/gateway-bridge.js +8 -9
- package/dist/gateway/gateway-timing-ledger.js +8 -6
- package/dist/gateway/openclaw-client.js +97 -297
- package/dist/gateway/sanitize-connect-reason.js +10 -0
- package/dist/gateway/sanitize-connect-reason.test.js +34 -0
- package/dist/index.js +3 -3
- package/dist/runtime/channel-two-hook.js +1 -6
- package/dist/runtime/container-env.js +1 -5
- package/dist/runtime/debug-bundle-handler.js +159 -0
- package/dist/runtime/display-toggle-states.js +6 -17
- package/dist/runtime/downstream-handler.js +682 -508
- package/dist/runtime/glasses-backpressure-latch.js +93 -0
- package/dist/runtime/ocuclaw-settings-store.js +10 -1
- package/dist/runtime/openclaw-host-version.js +5 -0
- package/dist/runtime/plugin-version-service.js +13 -6
- package/dist/runtime/provider-usage-select.js +0 -6
- package/dist/runtime/register-session-title-distiller.js +14 -16
- package/dist/runtime/relay-core.js +657 -271
- package/dist/runtime/relay-service.js +40 -36
- package/dist/runtime/relay-worker-approval-replay-cache.js +1 -1
- package/dist/runtime/relay-worker-entry.js +1 -2
- package/dist/runtime/relay-worker-health.js +2 -10
- package/dist/runtime/relay-worker-protocol.js +6 -1
- package/dist/runtime/relay-worker-supervisor.js +109 -39
- package/dist/runtime/relay-worker-transport.js +157 -15
- package/dist/runtime/session-context-service.js +5 -45
- package/dist/runtime/session-service.js +157 -175
- package/dist/runtime/session-title-distiller-budget.js +1 -5
- package/dist/runtime/session-title-distiller-helpers.js +14 -24
- package/dist/runtime/session-title-distiller.js +109 -122
- package/dist/runtime/session-title-record.js +0 -6
- package/dist/runtime/stable-prompt-snapshot.js +3 -14
- package/dist/runtime/upstream-runtime.js +600 -103
- package/dist/tools/device-info-tool.js +4 -21
- package/dist/tools/glasses-ui-cron.js +58 -63
- package/dist/tools/glasses-ui-descriptors.js +4 -33
- package/dist/tools/glasses-ui-limits.js +0 -13
- package/dist/tools/glasses-ui-paint-floor.js +22 -34
- package/dist/tools/glasses-ui-recipes.js +92 -101
- package/dist/tools/glasses-ui-surfaces.js +295 -100
- package/dist/tools/glasses-ui-template.js +7 -22
- package/dist/tools/glasses-ui-tool-description.test.js +2 -2
- package/dist/tools/glasses-ui-tool.js +475 -331
- package/dist/tools/glasses-ui-voicemail.js +242 -0
- package/dist/tools/glasses-ui-wake.js +195 -0
- package/dist/tools/session-title-tool.js +2 -7
- package/dist/tools/session-title-tool.test.js +1 -1
- package/dist/version.js +3 -2
- package/openclaw.plugin.json +60 -13
- package/package.json +3 -2
- package/skills/glasses-ui/SKILL.md +19 -3
- package/dist/runtime/protocol-adapter.js +0 -387
|
@@ -16,11 +16,19 @@ import { createWorkerMessageSendQueue } from "./relay-worker-queue.js";
|
|
|
16
16
|
import { createRelayWorkerHealthMonitor } from "./relay-worker-health.js";
|
|
17
17
|
import { createApprovalReplayCache } from "./relay-worker-approval-replay-cache.js";
|
|
18
18
|
import { createRelayClientNudgeController } from "./relay-client-nudge-controller.js";
|
|
19
|
+
import { constantTimeEqual } from "../domain/constant-time-equal.js";
|
|
19
20
|
|
|
20
21
|
const WebSocket = WebSocketModule.default || WebSocketModule.WebSocket || WebSocketModule;
|
|
21
22
|
const WebSocketServer = WebSocketModule.WebSocketServer || WebSocketModule.Server || WebSocket.Server;
|
|
22
23
|
const SEND_BUFFER_HIGH_WATER_BYTES = 262_144;
|
|
23
24
|
|
|
25
|
+
const SEND_BUFFER_HIGH_WATER_SHED_MS = 30_000;
|
|
26
|
+
|
|
27
|
+
const LIVENESS_MAX_MISSED_PINGS = 2;
|
|
28
|
+
|
|
29
|
+
const CONTROL_QUEUE_MAX_DEFAULT = 1000;
|
|
30
|
+
const TRANSACTIONAL_QUEUE_MAX_DEFAULT = 1000;
|
|
31
|
+
|
|
24
32
|
function normalizeLogger(logger) {
|
|
25
33
|
if (!logger || typeof logger !== "object") return console;
|
|
26
34
|
return {
|
|
@@ -52,12 +60,19 @@ export function createRelayWorkerTransport(options = {}) {
|
|
|
52
60
|
const logger = normalizeLogger(options.logger);
|
|
53
61
|
const postToMain = typeof options.postToMain === "function" ? options.postToMain : () => {};
|
|
54
62
|
const now = typeof options.now === "function" ? options.now : () => Date.now();
|
|
63
|
+
const listenRetryDelayMs =
|
|
64
|
+
Number.isFinite(options.listenRetryDelayMs) && options.listenRetryDelayMs >= 0
|
|
65
|
+
? Math.floor(options.listenRetryDelayMs)
|
|
66
|
+
: 200;
|
|
67
|
+
const listenRetryMaxAttempts =
|
|
68
|
+
Number.isFinite(options.listenRetryMaxAttempts) && options.listenRetryMaxAttempts >= 0
|
|
69
|
+
? Math.floor(options.listenRetryMaxAttempts)
|
|
70
|
+
: 5;
|
|
55
71
|
let manifest = null;
|
|
56
72
|
let httpServer = null;
|
|
57
73
|
let wss = null;
|
|
58
74
|
let nextClientId = 1;
|
|
59
|
-
|
|
60
|
-
// misconfiguration could flood, so they are rate-limited per remote address.
|
|
75
|
+
|
|
61
76
|
const TOKEN_REJECT_LOG_WINDOW_MS = 60000;
|
|
62
77
|
const TOKEN_REJECT_LOG_MAX_ADDRESSES = 100;
|
|
63
78
|
const tokenRejectLogState = new Map();
|
|
@@ -88,10 +103,13 @@ export function createRelayWorkerTransport(options = {}) {
|
|
|
88
103
|
}
|
|
89
104
|
let expireTimer = null;
|
|
90
105
|
let healthTimer = null;
|
|
106
|
+
let livenessTimer = null;
|
|
91
107
|
let loopDelayMonitor = null;
|
|
92
108
|
const clients = new Map();
|
|
93
109
|
const protocolState = new Map();
|
|
94
110
|
const outboundQueues = new Map();
|
|
111
|
+
|
|
112
|
+
const sendBufferOverWaterSince = new Map();
|
|
95
113
|
const sockets = new Set();
|
|
96
114
|
const pendingHttp = new Map();
|
|
97
115
|
const cache = {
|
|
@@ -223,14 +241,21 @@ export function createRelayWorkerTransport(options = {}) {
|
|
|
223
241
|
return !!normalizeRequestId(parsed && parsed.requestId);
|
|
224
242
|
}
|
|
225
243
|
|
|
244
|
+
function controlQueueMax() {
|
|
245
|
+
const v = manifest && manifest.queue ? manifest.queue.controlQueueMax : undefined;
|
|
246
|
+
return Number.isFinite(v) && v > 0 ? Math.floor(v) : CONTROL_QUEUE_MAX_DEFAULT;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function transactionalQueueMax() {
|
|
250
|
+
const v = manifest && manifest.queue ? manifest.queue.transactionalQueueMax : undefined;
|
|
251
|
+
return Number.isFinite(v) && v > 0 ? Math.floor(v) : TRANSACTIONAL_QUEUE_MAX_DEFAULT;
|
|
252
|
+
}
|
|
253
|
+
|
|
226
254
|
function enqueueFrame(clientId, frame, options = {}) {
|
|
227
255
|
const ws = clients.get(clientId);
|
|
228
256
|
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
229
257
|
const parsedFrame = parseFrame(frame);
|
|
230
|
-
|
|
231
|
-
// options.knownType from the broadcast/unicast call site) instead of
|
|
232
|
-
// re-deriving it per client. parsedFrame is still needed below for
|
|
233
|
-
// isImportantTransactionalFrame's requestId read, so parseFrame stays.
|
|
258
|
+
|
|
234
259
|
const type = options.knownType !== undefined ? options.knownType : parseMessageType(parsedFrame);
|
|
235
260
|
const q = ensureOutboundQueue(clientId);
|
|
236
261
|
if (
|
|
@@ -247,8 +272,26 @@ export function createRelayWorkerTransport(options = {}) {
|
|
|
247
272
|
type === APP_PROTOCOL.operationReceived
|
|
248
273
|
) {
|
|
249
274
|
q.control.push(frame);
|
|
275
|
+
while (q.control.length > controlQueueMax()) {
|
|
276
|
+
|
|
277
|
+
const dropped = q.control.shift();
|
|
278
|
+
emitDebug("worker_control_frame_dropped", "warn", {
|
|
279
|
+
clientId,
|
|
280
|
+
droppedType: parseMessageType(parseFrame(dropped)),
|
|
281
|
+
queueDepth: q.control.length,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
250
284
|
} else if (isImportantTransactionalFrame(type, parsedFrame)) {
|
|
251
285
|
q.transactional.push(frame);
|
|
286
|
+
while (q.transactional.length > transactionalQueueMax()) {
|
|
287
|
+
|
|
288
|
+
const dropped = q.transactional.shift();
|
|
289
|
+
emitDebug("worker_transactional_frame_dropped", "warn", {
|
|
290
|
+
clientId,
|
|
291
|
+
droppedType: parseMessageType(parseFrame(dropped)),
|
|
292
|
+
queueDepth: q.transactional.length,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
252
295
|
} else if (
|
|
253
296
|
type === APP_PROTOCOL.pages ||
|
|
254
297
|
type === APP_PROTOCOL.status ||
|
|
@@ -258,7 +301,7 @@ export function createRelayWorkerTransport(options = {}) {
|
|
|
258
301
|
} else {
|
|
259
302
|
q.bestEffort.push(frame);
|
|
260
303
|
while (q.bestEffort.length > 100) {
|
|
261
|
-
|
|
304
|
+
|
|
262
305
|
const dropped = q.bestEffort.shift();
|
|
263
306
|
emitDebug("worker_best_effort_frame_dropped", "warn", {
|
|
264
307
|
clientId,
|
|
@@ -600,7 +643,7 @@ export function createRelayWorkerTransport(options = {}) {
|
|
|
600
643
|
? parsed.hash
|
|
601
644
|
: null;
|
|
602
645
|
if (!requestedAgentName || !requestedHash) {
|
|
603
|
-
|
|
646
|
+
|
|
604
647
|
return;
|
|
605
648
|
}
|
|
606
649
|
const dataUri =
|
|
@@ -918,14 +961,15 @@ export function createRelayWorkerTransport(options = {}) {
|
|
|
918
961
|
sockets.delete(socket);
|
|
919
962
|
});
|
|
920
963
|
});
|
|
921
|
-
|
|
964
|
+
|
|
965
|
+
wss = new WebSocketServer({ noServer: true, maxPayload: manifest.rpc.wsMaxMessageBytes });
|
|
922
966
|
httpServer.on("upgrade", (req, socket, head) => {
|
|
923
967
|
wss.handleUpgrade(req, socket, head, (ws) => wss.emit("connection", ws, req));
|
|
924
968
|
});
|
|
925
969
|
wss.on("connection", (ws, req) => {
|
|
926
970
|
const requestUrl = new URL(req.url || "/", `http://${req.headers.host || "127.0.0.1"}`);
|
|
927
971
|
const remoteAddress = (req.socket && req.socket.remoteAddress) || "unknown";
|
|
928
|
-
if (requestUrl.searchParams.get("token")
|
|
972
|
+
if (!constantTimeEqual(requestUrl.searchParams.get("token"), manifest.relayToken)) {
|
|
929
973
|
logTokenReject(remoteAddress);
|
|
930
974
|
ws.close(4001, "invalid_token");
|
|
931
975
|
return;
|
|
@@ -936,6 +980,9 @@ export function createRelayWorkerTransport(options = {}) {
|
|
|
936
980
|
`[ocuclaw] relay client connected clientId=${clientId} remote=${remoteAddress}`,
|
|
937
981
|
);
|
|
938
982
|
clients.set(clientId, ws);
|
|
983
|
+
|
|
984
|
+
ws.__ocuMissedPings = 0;
|
|
985
|
+
ws.on("pong", () => { ws.__ocuMissedPings = 0; });
|
|
939
986
|
protocolState.set(clientId, {
|
|
940
987
|
protocolVersion: null,
|
|
941
988
|
clientKind: "unknown",
|
|
@@ -954,6 +1001,7 @@ export function createRelayWorkerTransport(options = {}) {
|
|
|
954
1001
|
clients.delete(clientId);
|
|
955
1002
|
protocolState.delete(clientId);
|
|
956
1003
|
outboundQueues.delete(clientId);
|
|
1004
|
+
sendBufferOverWaterSince.delete(clientId);
|
|
957
1005
|
if (nudgeController) nudgeController.deleteClient(clientId);
|
|
958
1006
|
const closeReasonStr =
|
|
959
1007
|
reason == null
|
|
@@ -978,11 +1026,43 @@ export function createRelayWorkerTransport(options = {}) {
|
|
|
978
1026
|
});
|
|
979
1027
|
|
|
980
1028
|
await new Promise((resolve, reject) => {
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1029
|
+
let attempt = 0;
|
|
1030
|
+
let settled = false;
|
|
1031
|
+
const onError = (err) => {
|
|
1032
|
+
if (settled) return;
|
|
1033
|
+
|
|
1034
|
+
if (err && err.code === "EADDRINUSE" && attempt < listenRetryMaxAttempts) {
|
|
1035
|
+
attempt += 1;
|
|
1036
|
+
emitDebug("worker_listen_retry", "warn", {
|
|
1037
|
+
attempt,
|
|
1038
|
+
code: err.code,
|
|
1039
|
+
port: manifest.port,
|
|
1040
|
+
});
|
|
1041
|
+
const retryTimer = setTimeout(tryListen, listenRetryDelayMs);
|
|
1042
|
+
if (typeof retryTimer.unref === "function") retryTimer.unref();
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
settled = true;
|
|
1046
|
+
reject(err);
|
|
1047
|
+
};
|
|
1048
|
+
const onListening = () => {
|
|
1049
|
+
if (settled) return;
|
|
1050
|
+
settled = true;
|
|
1051
|
+
httpServer.off("error", onError);
|
|
984
1052
|
resolve();
|
|
985
|
-
}
|
|
1053
|
+
};
|
|
1054
|
+
function tryListen() {
|
|
1055
|
+
if (settled) return;
|
|
1056
|
+
if (!httpServer) {
|
|
1057
|
+
|
|
1058
|
+
settled = true;
|
|
1059
|
+
reject(new Error("relay worker transport closed during listen retry"));
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
httpServer.once("error", onError);
|
|
1063
|
+
httpServer.listen(manifest.port, manifest.host, onListening);
|
|
1064
|
+
}
|
|
1065
|
+
tryListen();
|
|
986
1066
|
});
|
|
987
1067
|
loopDelayMonitor = monitorEventLoopDelay({ resolution: 50 });
|
|
988
1068
|
loopDelayMonitor.enable();
|
|
@@ -997,9 +1077,39 @@ export function createRelayWorkerTransport(options = {}) {
|
|
|
997
1077
|
}, Math.max(10, Math.min(1000, manifest.queue.messageSendTtlMs)));
|
|
998
1078
|
healthTimer = setInterval(() => {
|
|
999
1079
|
health.updateLoopLagP95Ms(sampleLoopLagP95Ms());
|
|
1000
|
-
|
|
1080
|
+
const sendBufferHighWaterClients = countSendBufferHighWaterClients();
|
|
1081
|
+
health.updateSendBufferHighWaterClients(sendBufferHighWaterClients);
|
|
1001
1082
|
health.sample();
|
|
1083
|
+
|
|
1084
|
+
sweepStuckSlowClients();
|
|
1085
|
+
|
|
1086
|
+
postToMain({
|
|
1087
|
+
kind: "worker.backpressure",
|
|
1088
|
+
workerEpoch: manifest.workerEpoch,
|
|
1089
|
+
sendBufferHighWaterClients,
|
|
1090
|
+
});
|
|
1002
1091
|
}, manifest.health.heartbeatIntervalMs);
|
|
1092
|
+
|
|
1093
|
+
const livenessIntervalMs =
|
|
1094
|
+
Number.isFinite(manifest.health.livenessIntervalMs) && manifest.health.livenessIntervalMs > 0
|
|
1095
|
+
? Math.floor(manifest.health.livenessIntervalMs)
|
|
1096
|
+
: Math.max(15000, manifest.health.heartbeatIntervalMs * 3);
|
|
1097
|
+
livenessTimer = setInterval(() => {
|
|
1098
|
+
for (const [, ws] of clients) {
|
|
1099
|
+
if (ws.readyState !== WebSocket.OPEN) continue;
|
|
1100
|
+
if ((ws.__ocuMissedPings || 0) >= LIVENESS_MAX_MISSED_PINGS) {
|
|
1101
|
+
ws.terminate();
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
ws.__ocuMissedPings = (ws.__ocuMissedPings || 0) + 1;
|
|
1105
|
+
try {
|
|
1106
|
+
ws.ping();
|
|
1107
|
+
} catch {
|
|
1108
|
+
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}, livenessIntervalMs);
|
|
1112
|
+
if (typeof livenessTimer.unref === "function") livenessTimer.unref();
|
|
1003
1113
|
postToMain({ kind: "worker.ready", workerEpoch: manifest.workerEpoch, address: address() });
|
|
1004
1114
|
}
|
|
1005
1115
|
|
|
@@ -1025,6 +1135,35 @@ export function createRelayWorkerTransport(options = {}) {
|
|
|
1025
1135
|
return count;
|
|
1026
1136
|
}
|
|
1027
1137
|
|
|
1138
|
+
function sweepStuckSlowClients() {
|
|
1139
|
+
const at = now();
|
|
1140
|
+
for (const [clientId, ws] of clients) {
|
|
1141
|
+
if ((protocolState.get(clientId) || {}).clientKind !== "app") continue;
|
|
1142
|
+
const over =
|
|
1143
|
+
ws.readyState === WebSocket.OPEN &&
|
|
1144
|
+
Number.isFinite(ws.bufferedAmount) &&
|
|
1145
|
+
ws.bufferedAmount > SEND_BUFFER_HIGH_WATER_BYTES;
|
|
1146
|
+
if (!over) {
|
|
1147
|
+
sendBufferOverWaterSince.delete(clientId);
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
const since = sendBufferOverWaterSince.get(clientId);
|
|
1151
|
+
if (since === undefined) {
|
|
1152
|
+
sendBufferOverWaterSince.set(clientId, at);
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
if (at - since >= SEND_BUFFER_HIGH_WATER_SHED_MS) {
|
|
1156
|
+
emitDebug("worker_client_send_buffer_shed", "warn", {
|
|
1157
|
+
clientId,
|
|
1158
|
+
bufferedAmount: ws.bufferedAmount,
|
|
1159
|
+
overHighWaterMs: at - since,
|
|
1160
|
+
});
|
|
1161
|
+
sendBufferOverWaterSince.delete(clientId);
|
|
1162
|
+
ws.terminate();
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1028
1167
|
function address() {
|
|
1029
1168
|
return httpServer && typeof httpServer.address === "function" ? httpServer.address() : null;
|
|
1030
1169
|
}
|
|
@@ -1032,8 +1171,10 @@ export function createRelayWorkerTransport(options = {}) {
|
|
|
1032
1171
|
function close() {
|
|
1033
1172
|
if (expireTimer) clearInterval(expireTimer);
|
|
1034
1173
|
if (healthTimer) clearInterval(healthTimer);
|
|
1174
|
+
if (livenessTimer) clearInterval(livenessTimer);
|
|
1035
1175
|
expireTimer = null;
|
|
1036
1176
|
healthTimer = null;
|
|
1177
|
+
livenessTimer = null;
|
|
1037
1178
|
if (loopDelayMonitor) {
|
|
1038
1179
|
loopDelayMonitor.disable();
|
|
1039
1180
|
loopDelayMonitor = null;
|
|
@@ -1049,6 +1190,7 @@ export function createRelayWorkerTransport(options = {}) {
|
|
|
1049
1190
|
clients.clear();
|
|
1050
1191
|
protocolState.clear();
|
|
1051
1192
|
outboundQueues.clear();
|
|
1193
|
+
sendBufferOverWaterSince.clear();
|
|
1052
1194
|
if (nudgeController) nudgeController.clear();
|
|
1053
1195
|
nudgeController = null;
|
|
1054
1196
|
if (approvalReplay) approvalReplay.clear();
|
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
// session-context-service.ts
|
|
2
|
-
//
|
|
3
|
-
// Owns the active-session "context tokens vs context window" snapshot
|
|
4
|
-
// lifecycle. Reads via gateway sessions.describe; reads compaction
|
|
5
|
-
// checkpoint count via sessions.compaction.list; broadcasts on changes.
|
|
6
|
-
//
|
|
7
|
-
// Field mapping (gateway → snapshot):
|
|
8
|
-
// session.totalTokens → contextTokens (current cumulative usage)
|
|
9
|
-
// session.contextTokens → contextWindow (model's configured window)
|
|
10
|
-
// openclaw uses `contextTokens` on the row for window size; `totalTokens`
|
|
11
|
-
// is the running usage. NOTE: the window is NOT resolved on a fresh session —
|
|
12
|
-
// the gateway warms a model's window only after the first turn (sessions.describe
|
|
13
|
-
// returns no window pre-turn). So we cache the observed window per model and
|
|
14
|
-
// reuse it for subsequent cold/new-session reads. The cache is persisted under
|
|
15
|
-
// `stateDir` (keyed by provider/model) so it survives relay restarts.
|
|
16
|
-
|
|
17
1
|
import * as fs from "node:fs";
|
|
18
2
|
import * as path from "node:path";
|
|
19
3
|
|
|
@@ -42,7 +26,7 @@ function loadModelContextWindowCache(cachePath) {
|
|
|
42
26
|
}
|
|
43
27
|
}
|
|
44
28
|
} catch {
|
|
45
|
-
|
|
29
|
+
|
|
46
30
|
}
|
|
47
31
|
return cache;
|
|
48
32
|
}
|
|
@@ -55,29 +39,10 @@ function persistModelContextWindowCache(cachePath, cache) {
|
|
|
55
39
|
for (const [key, value] of cache.entries()) obj[key] = value;
|
|
56
40
|
fs.writeFileSync(cachePath, JSON.stringify(obj), "utf8");
|
|
57
41
|
} catch {
|
|
58
|
-
|
|
42
|
+
|
|
59
43
|
}
|
|
60
44
|
}
|
|
61
45
|
|
|
62
|
-
/**
|
|
63
|
-
* @typedef SessionContextSnapshot
|
|
64
|
-
* @property {string} type
|
|
65
|
-
* @property {string} sessionKey
|
|
66
|
-
* @property {number} contextTokens
|
|
67
|
-
* @property {number} contextWindow
|
|
68
|
-
* @property {number} compactionCount
|
|
69
|
-
* @property {boolean} runActive
|
|
70
|
-
* @property {number} snapshotAtMs
|
|
71
|
-
*/
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* @param {object} opts
|
|
75
|
-
* @param {{request: (method: string, params: object) => Promise<any>}} opts.gatewayBridge
|
|
76
|
-
* @param {() => string|null} opts.getActiveSessionKey
|
|
77
|
-
* @param {() => boolean} opts.getRunActive
|
|
78
|
-
* @param {() => number} opts.nowMs
|
|
79
|
-
* @param {(frame: SessionContextSnapshot) => void} opts.broadcast
|
|
80
|
-
*/
|
|
81
46
|
export function createSessionContextService(opts) {
|
|
82
47
|
const gatewayBridge = opts.gatewayBridge;
|
|
83
48
|
const getActiveSessionKey = opts.getActiveSessionKey;
|
|
@@ -87,15 +52,10 @@ export function createSessionContextService(opts) {
|
|
|
87
52
|
const getActiveModelKey =
|
|
88
53
|
typeof opts.getActiveModelKey === "function" ? opts.getActiveModelKey : () => null;
|
|
89
54
|
|
|
90
|
-
/** @type {SessionContextSnapshot|null} */
|
|
91
55
|
let lastSnapshot = null;
|
|
92
56
|
|
|
93
|
-
// Per-model context-window cache: the gateway only resolves a model's window
|
|
94
|
-
// after the first turn warms its discovery, so once we observe a real window
|
|
95
|
-
// we remember it per model and reuse it for later cold/new-session reads.
|
|
96
|
-
// Persisted under stateDir so it survives relay restarts.
|
|
97
57
|
const modelContextWindowCachePath = resolveModelContextWindowCachePath(opts.stateDir);
|
|
98
|
-
|
|
58
|
+
|
|
99
59
|
const modelContextWindowCache = loadModelContextWindowCache(modelContextWindowCachePath);
|
|
100
60
|
|
|
101
61
|
async function refreshActiveSessionContext() {
|
|
@@ -128,13 +88,13 @@ export function createSessionContextService(opts) {
|
|
|
128
88
|
const modelKey = getActiveModelKey();
|
|
129
89
|
let contextWindow = describeWindow;
|
|
130
90
|
if (describeWindow > 0) {
|
|
131
|
-
|
|
91
|
+
|
|
132
92
|
if (modelKey && modelContextWindowCache.get(modelKey) !== describeWindow) {
|
|
133
93
|
modelContextWindowCache.set(modelKey, describeWindow);
|
|
134
94
|
persistModelContextWindowCache(modelContextWindowCachePath, modelContextWindowCache);
|
|
135
95
|
}
|
|
136
96
|
} else if (modelKey && modelContextWindowCache.has(modelKey)) {
|
|
137
|
-
|
|
97
|
+
|
|
138
98
|
contextWindow = modelContextWindowCache.get(modelKey);
|
|
139
99
|
}
|
|
140
100
|
const checkpoints =
|