ocuclaw 0.1.0 → 1.2.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 +48 -6
- package/dist/domain/debug-store.js +3 -2
- package/dist/runtime/downstream-handler.js +176 -6
- package/dist/runtime/downstream-server.js +326 -0
- package/dist/runtime/relay-core.js +149 -7
- package/dist/runtime/session-service.js +42 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# OcuClaw
|
|
2
2
|
|
|
3
|
-
OcuClaw is an OpenClaw plugin for Even G2 smart glasses.
|
|
3
|
+
OcuClaw is an OpenClaw plugin for Even G2 smart glasses. Use the OcuClaw application within Even Hub App Store to connect the client side.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
+
Install the plugin from the OpenClaw CLI:
|
|
8
|
+
|
|
7
9
|
```bash
|
|
8
10
|
openclaw plugins install ocuclaw
|
|
9
11
|
openclaw plugins enable ocuclaw
|
|
@@ -11,15 +13,55 @@ openclaw plugins enable ocuclaw
|
|
|
11
13
|
|
|
12
14
|
## Configure
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
Required:
|
|
17
|
+
|
|
18
|
+
Set the OcuClaw relay token. This is a user-created password that must match the relay server token field in the OcuClaw application within Even Hub App Store.
|
|
15
19
|
|
|
16
20
|
```bash
|
|
17
21
|
openclaw config set plugins.entries.ocuclaw.config.relayToken "<relay-token>"
|
|
18
22
|
```
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
Recommended:
|
|
25
|
+
|
|
26
|
+
- `sonioxApiKey`: Enables Soniox speech-to-text for voice input.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
openclaw config set plugins.entries.ocuclaw.config.sonioxApiKey "<soniox-api-key>"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- `evenAiEnabled`: Enables Even AI integration for OcuClaw.
|
|
21
33
|
|
|
22
|
-
|
|
34
|
+
```bash
|
|
35
|
+
openclaw config set plugins.entries.ocuclaw.config.evenAiEnabled true --strict-json
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- `evenAiToken`: Sets the user-created password for Even AI requests. This must match the password set in the Even AI Agent Configure section within the Even Realities app.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
openclaw config set plugins.entries.ocuclaw.config.evenAiToken "<even-ai-token>"
|
|
42
|
+
```
|
|
23
43
|
|
|
24
|
-
|
|
25
|
-
|
|
44
|
+
Advanced optional settings:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
openclaw config set plugins.entries.ocuclaw.config.wsBind "127.0.0.1"
|
|
48
|
+
openclaw config set plugins.entries.ocuclaw.config.wsPort 9000 --strict-json
|
|
49
|
+
openclaw config set plugins.entries.ocuclaw.config.sessionLimit 10 --strict-json
|
|
50
|
+
openclaw config set plugins.entries.ocuclaw.config.externalDebugToolsEnabled true --strict-json
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Restart
|
|
54
|
+
|
|
55
|
+
Restart the gateway so the plugin and config changes take effect:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
openclaw gateway restart
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Verify
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
openclaw plugins inspect ocuclaw
|
|
65
|
+
openclaw plugins doctor
|
|
66
|
+
openclaw gateway status
|
|
67
|
+
```
|
|
@@ -458,11 +458,12 @@ function redactStringValue(value, mode, keyName) {
|
|
|
458
458
|
}
|
|
459
459
|
}
|
|
460
460
|
|
|
461
|
-
function emit(event) {
|
|
461
|
+
function emit(event, options) {
|
|
462
462
|
const raw = event || {};
|
|
463
463
|
const cat = typeof raw.cat === "string" ? raw.cat.trim() : "";
|
|
464
|
+
const force = !!(options && options.force === true);
|
|
464
465
|
if (!cat) return false;
|
|
465
|
-
if (!isEnabled(cat)) return false;
|
|
466
|
+
if (!force && !isEnabled(cat)) return false;
|
|
466
467
|
|
|
467
468
|
const ts = Number.isFinite(raw.ts) ? Math.floor(raw.ts) : nowFn();
|
|
468
469
|
const eventName =
|
|
@@ -63,6 +63,8 @@ function normalizeLogger(logger) {
|
|
|
63
63
|
* Returns true if the OpenClaw gateway connection is active.
|
|
64
64
|
* @param {(clientId: string, payload: object) => void} [opts.onEventDebug]
|
|
65
65
|
* Optional structured client debug-event callback.
|
|
66
|
+
* @param {(clientId: string, payload: object) => Promise<object>|object} [opts.onReadinessProbe]
|
|
67
|
+
* Optional dedicated readiness-probe dispatcher.
|
|
66
68
|
* @returns {object} Handler instance
|
|
67
69
|
*/
|
|
68
70
|
function createDownstreamHandler(opts) {
|
|
@@ -95,6 +97,7 @@ function createDownstreamHandler(opts) {
|
|
|
95
97
|
const onDebugDump = opts.onDebugDump || null;
|
|
96
98
|
const onEventDebug = opts.onEventDebug || null;
|
|
97
99
|
const onRemoteControl = opts.onRemoteControl || null;
|
|
100
|
+
const onReadinessProbe = opts.onReadinessProbe || null;
|
|
98
101
|
const getSnapshotRevision = opts.getSnapshotRevision || null;
|
|
99
102
|
|
|
100
103
|
/** Client IDs subscribed to raw protocol frame forwarding. */
|
|
@@ -139,6 +142,8 @@ function createDownstreamHandler(opts) {
|
|
|
139
142
|
pages: "ocuclaw.view.pages.snapshot",
|
|
140
143
|
protocolSubscribe: "ocuclaw.protocol.tap.subscribe",
|
|
141
144
|
protocolFrame: "ocuclaw.protocol.tap.frame",
|
|
145
|
+
readinessProbeAck: "ocuclaw.readiness.probe.ack",
|
|
146
|
+
readinessProbeRequest: "ocuclaw.readiness.probe.request",
|
|
142
147
|
remoteControl: "ocuclaw.remote.control",
|
|
143
148
|
requestSonioxTemporaryKey: "requestSonioxTemporaryKey",
|
|
144
149
|
sonioxModelsGet: "ocuclaw.voice.soniox.models.get",
|
|
@@ -234,7 +239,8 @@ function createDownstreamHandler(opts) {
|
|
|
234
239
|
return (
|
|
235
240
|
messageType === "debug-set" ||
|
|
236
241
|
messageType === "debug-dump" ||
|
|
237
|
-
messageType === "remote-control"
|
|
242
|
+
messageType === "remote-control" ||
|
|
243
|
+
messageType === APP_PROTOCOL.readinessProbeRequest
|
|
238
244
|
);
|
|
239
245
|
}
|
|
240
246
|
|
|
@@ -827,6 +833,63 @@ function createDownstreamHandler(opts) {
|
|
|
827
833
|
});
|
|
828
834
|
}
|
|
829
835
|
|
|
836
|
+
function formatReadinessProbeRequest(data) {
|
|
837
|
+
return JSON.stringify({
|
|
838
|
+
type: APP_PROTOCOL.readinessProbeRequest,
|
|
839
|
+
requestId:
|
|
840
|
+
data && typeof data.requestId === "string" && data.requestId
|
|
841
|
+
? data.requestId
|
|
842
|
+
: null,
|
|
843
|
+
sinceMs:
|
|
844
|
+
data && Number.isFinite(Number(data.sinceMs))
|
|
845
|
+
? Math.max(0, Math.floor(Number(data.sinceMs)))
|
|
846
|
+
: 0,
|
|
847
|
+
sessionKey:
|
|
848
|
+
data && typeof data.sessionKey === "string" && data.sessionKey
|
|
849
|
+
? data.sessionKey
|
|
850
|
+
: null,
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function formatReadinessProbeAck(data) {
|
|
855
|
+
return JSON.stringify({
|
|
856
|
+
type: APP_PROTOCOL.readinessProbeAck,
|
|
857
|
+
ok: data && data.ok !== false,
|
|
858
|
+
requestId:
|
|
859
|
+
data && typeof data.requestId === "string" && data.requestId
|
|
860
|
+
? data.requestId
|
|
861
|
+
: null,
|
|
862
|
+
reasonCode:
|
|
863
|
+
data && typeof data.reasonCode === "string" && data.reasonCode
|
|
864
|
+
? data.reasonCode
|
|
865
|
+
: null,
|
|
866
|
+
message:
|
|
867
|
+
data && typeof data.message === "string" && data.message
|
|
868
|
+
? data.message
|
|
869
|
+
: null,
|
|
870
|
+
activeSessionKey:
|
|
871
|
+
data && typeof data.activeSessionKey === "string" && data.activeSessionKey
|
|
872
|
+
? data.activeSessionKey
|
|
873
|
+
: null,
|
|
874
|
+
emittedAtMs:
|
|
875
|
+
data && Number.isFinite(Number(data.emittedAtMs))
|
|
876
|
+
? Math.max(0, Math.floor(Number(data.emittedAtMs)))
|
|
877
|
+
: null,
|
|
878
|
+
clientId:
|
|
879
|
+
data && typeof data.clientId === "string" && data.clientId
|
|
880
|
+
? data.clientId
|
|
881
|
+
: null,
|
|
882
|
+
clientName:
|
|
883
|
+
data && typeof data.clientName === "string" && data.clientName
|
|
884
|
+
? data.clientName
|
|
885
|
+
: null,
|
|
886
|
+
clientVersion:
|
|
887
|
+
data && typeof data.clientVersion === "string" && data.clientVersion
|
|
888
|
+
? data.clientVersion
|
|
889
|
+
: null,
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
830
893
|
function normalizeCategories(raw, fieldName) {
|
|
831
894
|
if (raw === undefined || raw === null) return [];
|
|
832
895
|
if (!Array.isArray(raw)) {
|
|
@@ -1124,6 +1187,26 @@ function createDownstreamHandler(opts) {
|
|
|
1124
1187
|
) {
|
|
1125
1188
|
return "debug-close-app-client";
|
|
1126
1189
|
}
|
|
1190
|
+
if (
|
|
1191
|
+
normalized === "debug-screen-off-worker-on" ||
|
|
1192
|
+
normalized === "debug_screen_off_worker_on" ||
|
|
1193
|
+
normalized === "debugscreenoffworkeron" ||
|
|
1194
|
+
normalized === "screen-off-worker-on" ||
|
|
1195
|
+
normalized === "screen_off_worker_on" ||
|
|
1196
|
+
normalized === "screenoffworkeron"
|
|
1197
|
+
) {
|
|
1198
|
+
return "debug-screen-off-worker-on";
|
|
1199
|
+
}
|
|
1200
|
+
if (
|
|
1201
|
+
normalized === "debug-screen-off-worker-off" ||
|
|
1202
|
+
normalized === "debug_screen_off_worker_off" ||
|
|
1203
|
+
normalized === "debugscreenoffworkeroff" ||
|
|
1204
|
+
normalized === "screen-off-worker-off" ||
|
|
1205
|
+
normalized === "screen_off_worker_off" ||
|
|
1206
|
+
normalized === "screenoffworkeroff"
|
|
1207
|
+
) {
|
|
1208
|
+
return "debug-screen-off-worker-off";
|
|
1209
|
+
}
|
|
1127
1210
|
throw new Error(`unsupported remote relayAction: ${raw}`);
|
|
1128
1211
|
}
|
|
1129
1212
|
|
|
@@ -1187,12 +1270,19 @@ function createDownstreamHandler(opts) {
|
|
|
1187
1270
|
|
|
1188
1271
|
const payload = {};
|
|
1189
1272
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1273
|
+
if (Object.prototype.hasOwnProperty.call(msg, "modelRef")) {
|
|
1274
|
+
if (typeof msg.modelRef !== "string") {
|
|
1275
|
+
throw new Error("modelRef must be in provider/id format or blank");
|
|
1276
|
+
}
|
|
1277
|
+
const modelRef = msg.modelRef.trim();
|
|
1278
|
+
if (!modelRef) {
|
|
1279
|
+
payload.modelRef = "";
|
|
1280
|
+
} else {
|
|
1281
|
+
if (!modelRef.includes("/")) {
|
|
1282
|
+
throw new Error("modelRef must be in provider/id format");
|
|
1283
|
+
}
|
|
1284
|
+
payload.modelRef = modelRef;
|
|
1194
1285
|
}
|
|
1195
|
-
payload.modelRef = modelRef;
|
|
1196
1286
|
}
|
|
1197
1287
|
|
|
1198
1288
|
if (Object.prototype.hasOwnProperty.call(msg, "thinkingLevel")) {
|
|
@@ -1489,6 +1579,25 @@ function createDownstreamHandler(opts) {
|
|
|
1489
1579
|
throw new Error(`unsupported remote-control action: ${actionRaw}`);
|
|
1490
1580
|
}
|
|
1491
1581
|
|
|
1582
|
+
function parseReadinessProbe(msg) {
|
|
1583
|
+
if (!msg || typeof msg !== "object") {
|
|
1584
|
+
throw new Error("readiness probe payload must be an object");
|
|
1585
|
+
}
|
|
1586
|
+
const requestId = parseOptionalTrimmedString(msg.requestId);
|
|
1587
|
+
if (!requestId) {
|
|
1588
|
+
throw new Error("readiness probe requires requestId");
|
|
1589
|
+
}
|
|
1590
|
+
const sinceMs = parseOptionalNonNegativeNumber(msg.sinceMs, "sinceMs");
|
|
1591
|
+
if (sinceMs === undefined) {
|
|
1592
|
+
throw new Error("readiness probe sinceMs must be a non-negative number");
|
|
1593
|
+
}
|
|
1594
|
+
return {
|
|
1595
|
+
requestId,
|
|
1596
|
+
sinceMs,
|
|
1597
|
+
sessionKey: parseOptionalTrimmedString(msg.sessionKey) || null,
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1492
1601
|
const ATTACHMENT_MAX_DECODED_BYTES = 5_000_000;
|
|
1493
1602
|
const ATTACHMENT_MAX_ENCODED_CHARS =
|
|
1494
1603
|
Math.ceil((ATTACHMENT_MAX_DECODED_BYTES * 4) / 3) + 16;
|
|
@@ -2591,6 +2700,63 @@ function createDownstreamHandler(opts) {
|
|
|
2591
2700
|
}
|
|
2592
2701
|
}
|
|
2593
2702
|
|
|
2703
|
+
function handleReadinessProbe(clientId, msg) {
|
|
2704
|
+
if (!onReadinessProbe) {
|
|
2705
|
+
return { unicast: formatError("readiness probe is not available") };
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
let payload;
|
|
2709
|
+
try {
|
|
2710
|
+
payload = parseReadinessProbe(msg);
|
|
2711
|
+
} catch (err) {
|
|
2712
|
+
return { unicast: formatError(err.message) };
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
const finalize = (result) => {
|
|
2716
|
+
const resolved = result || {};
|
|
2717
|
+
const requestId = resolved.requestId || payload.requestId;
|
|
2718
|
+
if (
|
|
2719
|
+
resolved.ok === false ||
|
|
2720
|
+
!resolved.targetClientId ||
|
|
2721
|
+
!resolved.probe
|
|
2722
|
+
) {
|
|
2723
|
+
return {
|
|
2724
|
+
unicast: formatReadinessProbeAck({
|
|
2725
|
+
ok: false,
|
|
2726
|
+
requestId,
|
|
2727
|
+
reasonCode: resolved.reasonCode || null,
|
|
2728
|
+
message: resolved.message || "readiness probe was not dispatched",
|
|
2729
|
+
activeSessionKey: resolved.activeSessionKey || null,
|
|
2730
|
+
emittedAtMs: resolved.emittedAtMs || null,
|
|
2731
|
+
}),
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
return {
|
|
2736
|
+
readinessProbe: {
|
|
2737
|
+
requestId,
|
|
2738
|
+
targetClientId: resolved.targetClientId,
|
|
2739
|
+
message: formatReadinessProbeRequest(resolved.probe),
|
|
2740
|
+
},
|
|
2741
|
+
};
|
|
2742
|
+
};
|
|
2743
|
+
|
|
2744
|
+
try {
|
|
2745
|
+
const result = onReadinessProbe(clientId, payload);
|
|
2746
|
+
if (result && typeof result.then === "function") {
|
|
2747
|
+
return result.then(
|
|
2748
|
+
(resolved) => finalize(resolved),
|
|
2749
|
+
(err) => ({
|
|
2750
|
+
unicast: formatError(err.message || "readiness probe failed"),
|
|
2751
|
+
}),
|
|
2752
|
+
);
|
|
2753
|
+
}
|
|
2754
|
+
return finalize(result);
|
|
2755
|
+
} catch (err) {
|
|
2756
|
+
return { unicast: formatError(err.message || "readiness probe failed") };
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2594
2760
|
// --- Public API ---
|
|
2595
2761
|
|
|
2596
2762
|
return {
|
|
@@ -2681,6 +2847,8 @@ function createDownstreamHandler(opts) {
|
|
|
2681
2847
|
return handleDebugDump(clientId, msg);
|
|
2682
2848
|
case "remote-control":
|
|
2683
2849
|
return handleRemoteControl(clientId, msg);
|
|
2850
|
+
case APP_PROTOCOL.readinessProbeRequest:
|
|
2851
|
+
return handleReadinessProbe(clientId, msg);
|
|
2684
2852
|
case APP_PROTOCOL.debugEvent:
|
|
2685
2853
|
return handleEventDebug(clientId, msg);
|
|
2686
2854
|
default:
|
|
@@ -2721,6 +2889,8 @@ function createDownstreamHandler(opts) {
|
|
|
2721
2889
|
formatDebugConfigSnapshot,
|
|
2722
2890
|
formatRemoteControl,
|
|
2723
2891
|
formatRemoteControlAck,
|
|
2892
|
+
formatReadinessProbeRequest,
|
|
2893
|
+
formatReadinessProbeAck,
|
|
2724
2894
|
formatError,
|
|
2725
2895
|
|
|
2726
2896
|
/**
|
|
@@ -97,6 +97,10 @@ function createDownstreamServer(opts) {
|
|
|
97
97
|
const syncState = new Map();
|
|
98
98
|
/** @type {Map<string, {visibilityState: "hidden"|"visible"|"blurred"|null, streamChars: number|null, lastHeartbeatAtMs: number|null, lastRelayStreamingActivityAtMs: number|null, interactionStage: "idle"|"listening"|"voice_handoff"|"thinking"|"streaming"|"post_turn_drain", cadenceBucket: "idle"|"active_non_stream"|"active_stream", nudgeActive: boolean, nudgeIntervalMs: number|null, nudgeStartedAtMs: number|null, lastNudgeAtMs: number|null, stalledHeartbeatCount: number, nudgeTimer: any, idleDeactivateTimer: any, staleHeartbeatTimer: any, hardTimeoutTimer: any}>} */
|
|
99
99
|
const clientNudgeState = new Map();
|
|
100
|
+
/** @type {Map<string, {clientDebugEnabled: boolean|null, runtimeDiagnosticsVisible: boolean|null, perfNoisyDebugMuted: boolean|null, perfPayloadLiteMode: boolean|null, activeSessionKey: string|null, bundleIdentity: {kind: string|null, mode: string|null, host: string|null, port: number|null, servedDistPath: string|null}|null, emittedAtMs: number|null, updatedAtMs: number}>} */
|
|
101
|
+
const clientReadinessSnapshotState = new Map();
|
|
102
|
+
/** @type {Map<string, {requesterClientId: string, targetClientId: string, createdAtMs: number}>} */
|
|
103
|
+
const pendingReadinessProbeRequests = new Map();
|
|
100
104
|
/** @type {Map<string, string>} */
|
|
101
105
|
const unresolvedApprovals = new Map();
|
|
102
106
|
let nextClientId = 1;
|
|
@@ -105,6 +109,9 @@ function createDownstreamServer(opts) {
|
|
|
105
109
|
approvalResolved: "ocuclaw.approval.resolved",
|
|
106
110
|
debugConfigSnapshot: "ocuclaw.debug.config.snapshot",
|
|
107
111
|
pages: "ocuclaw.view.pages.snapshot",
|
|
112
|
+
readinessProbeAck: "ocuclaw.readiness.probe.ack",
|
|
113
|
+
readinessProbeRequest: "ocuclaw.readiness.probe.request",
|
|
114
|
+
readinessSnapshot: "ocuclaw.readiness.snapshot",
|
|
108
115
|
resume: "ocuclaw.sync.resume",
|
|
109
116
|
resumeAck: "ocuclaw.sync.resume.ack",
|
|
110
117
|
status: "ocuclaw.runtime.status",
|
|
@@ -174,6 +181,20 @@ function createDownstreamServer(opts) {
|
|
|
174
181
|
return normalized === "true" || normalized === "1" || normalized === "yes";
|
|
175
182
|
}
|
|
176
183
|
|
|
184
|
+
function parseOptionalBoolean(value) {
|
|
185
|
+
if (value === true || value === false) return value;
|
|
186
|
+
if (typeof value === "number") return value !== 0;
|
|
187
|
+
if (typeof value !== "string") return null;
|
|
188
|
+
const normalized = value.trim().toLowerCase();
|
|
189
|
+
if (normalized === "true" || normalized === "1" || normalized === "yes") {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
if (normalized === "false" || normalized === "0" || normalized === "no") {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
177
198
|
function parseOptionalTrimmedString(value) {
|
|
178
199
|
if (typeof value !== "string") return null;
|
|
179
200
|
const trimmed = value.trim();
|
|
@@ -262,6 +283,90 @@ function createDownstreamServer(opts) {
|
|
|
262
283
|
parseOptionalTrimmedString(value.clientId),
|
|
263
284
|
clientVersion: parseOptionalTrimmedString(value.clientVersion),
|
|
264
285
|
sessionKey: parseOptionalTrimmedString(value.sessionKey),
|
|
286
|
+
readinessSnapshot: parseReadinessSnapshot(value.readinessSnapshot),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function parseReadinessBundleIdentity(value) {
|
|
291
|
+
if (!value || typeof value !== "object") {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
const kind = parseOptionalTrimmedString(value.kind || value.lane);
|
|
295
|
+
const mode = parseOptionalTrimmedString(value.mode);
|
|
296
|
+
const host = parseOptionalTrimmedString(value.host);
|
|
297
|
+
const port = parseOptionalNonNegativeInt(value.port);
|
|
298
|
+
const servedDistPath =
|
|
299
|
+
parseOptionalTrimmedString(value.servedDistPath) ||
|
|
300
|
+
parseOptionalTrimmedString(value.staticDir);
|
|
301
|
+
if (!kind && !mode && !host && port === null && !servedDistPath) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
kind: kind || null,
|
|
306
|
+
mode: mode || null,
|
|
307
|
+
host: host || null,
|
|
308
|
+
port,
|
|
309
|
+
servedDistPath: servedDistPath || null,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function parseReadinessSnapshot(value) {
|
|
314
|
+
if (!value || typeof value !== "object") {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
const clientDebugEnabled = parseOptionalBoolean(value.clientDebugEnabled);
|
|
318
|
+
const runtimeDiagnosticsVisible = parseOptionalBoolean(
|
|
319
|
+
value.runtimeDiagnosticsVisible,
|
|
320
|
+
);
|
|
321
|
+
const perfNoisyDebugMuted = parseOptionalBoolean(value.perfNoisyDebugMuted);
|
|
322
|
+
const perfPayloadLiteMode = parseOptionalBoolean(value.perfPayloadLiteMode);
|
|
323
|
+
const activeSessionKey =
|
|
324
|
+
parseOptionalTrimmedString(value.activeSessionKey) ||
|
|
325
|
+
parseOptionalTrimmedString(value.sessionKey);
|
|
326
|
+
const bundleIdentity = parseReadinessBundleIdentity(
|
|
327
|
+
value.bundleIdentity || value.bundle,
|
|
328
|
+
);
|
|
329
|
+
const emittedAtMs = parseOptionalNonNegativeInt(value.emittedAtMs);
|
|
330
|
+
if (
|
|
331
|
+
clientDebugEnabled === null &&
|
|
332
|
+
runtimeDiagnosticsVisible === null &&
|
|
333
|
+
perfNoisyDebugMuted === null &&
|
|
334
|
+
perfPayloadLiteMode === null &&
|
|
335
|
+
!activeSessionKey &&
|
|
336
|
+
!bundleIdentity &&
|
|
337
|
+
emittedAtMs === null
|
|
338
|
+
) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
clientDebugEnabled,
|
|
343
|
+
runtimeDiagnosticsVisible,
|
|
344
|
+
perfNoisyDebugMuted,
|
|
345
|
+
perfPayloadLiteMode,
|
|
346
|
+
activeSessionKey: activeSessionKey || null,
|
|
347
|
+
bundleIdentity,
|
|
348
|
+
emittedAtMs,
|
|
349
|
+
updatedAtMs: Date.now(),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function parseReadinessProbeAck(value) {
|
|
354
|
+
if (!value || value.type !== APP_PROTOCOL.readinessProbeAck) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
const requestId = parseOptionalTrimmedString(value.requestId);
|
|
358
|
+
if (!requestId) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
requestId,
|
|
363
|
+
ok: value.ok !== false,
|
|
364
|
+
reasonCode: parseOptionalTrimmedString(value.reasonCode),
|
|
365
|
+
message: parseOptionalTrimmedString(value.message),
|
|
366
|
+
activeSessionKey:
|
|
367
|
+
parseOptionalTrimmedString(value.activeSessionKey) ||
|
|
368
|
+
parseOptionalTrimmedString(value.sessionKey),
|
|
369
|
+
emittedAtMs: parseOptionalNonNegativeInt(value.emittedAtMs),
|
|
265
370
|
};
|
|
266
371
|
}
|
|
267
372
|
|
|
@@ -449,6 +554,81 @@ function createDownstreamServer(opts) {
|
|
|
449
554
|
};
|
|
450
555
|
}
|
|
451
556
|
|
|
557
|
+
function cloneReadinessBundleIdentity(value) {
|
|
558
|
+
if (!value) return null;
|
|
559
|
+
return {
|
|
560
|
+
kind: value.kind || null,
|
|
561
|
+
mode: value.mode || null,
|
|
562
|
+
host: value.host || null,
|
|
563
|
+
port: Number.isFinite(value.port) ? value.port : null,
|
|
564
|
+
servedDistPath: value.servedDistPath || null,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function cloneReadinessSnapshot(value) {
|
|
569
|
+
if (!value) return null;
|
|
570
|
+
return {
|
|
571
|
+
clientDebugEnabled:
|
|
572
|
+
value.clientDebugEnabled === true || value.clientDebugEnabled === false
|
|
573
|
+
? value.clientDebugEnabled
|
|
574
|
+
: null,
|
|
575
|
+
runtimeDiagnosticsVisible:
|
|
576
|
+
value.runtimeDiagnosticsVisible === true ||
|
|
577
|
+
value.runtimeDiagnosticsVisible === false
|
|
578
|
+
? value.runtimeDiagnosticsVisible
|
|
579
|
+
: null,
|
|
580
|
+
perfNoisyDebugMuted:
|
|
581
|
+
value.perfNoisyDebugMuted === true || value.perfNoisyDebugMuted === false
|
|
582
|
+
? value.perfNoisyDebugMuted
|
|
583
|
+
: null,
|
|
584
|
+
perfPayloadLiteMode:
|
|
585
|
+
value.perfPayloadLiteMode === true || value.perfPayloadLiteMode === false
|
|
586
|
+
? value.perfPayloadLiteMode
|
|
587
|
+
: null,
|
|
588
|
+
activeSessionKey: value.activeSessionKey || null,
|
|
589
|
+
bundleIdentity: cloneReadinessBundleIdentity(value.bundleIdentity),
|
|
590
|
+
emittedAtMs: Number.isFinite(value.emittedAtMs) ? value.emittedAtMs : null,
|
|
591
|
+
updatedAtMs: Number.isFinite(value.updatedAtMs) ? value.updatedAtMs : null,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function updateClientReadinessSnapshot(clientId, snapshot) {
|
|
596
|
+
if (!clientId || !snapshot) return null;
|
|
597
|
+
const next = cloneReadinessSnapshot({
|
|
598
|
+
...snapshot,
|
|
599
|
+
updatedAtMs: Date.now(),
|
|
600
|
+
});
|
|
601
|
+
clientReadinessSnapshotState.set(clientId, next);
|
|
602
|
+
return next;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function clearPendingReadinessProbesForClient(clientId) {
|
|
606
|
+
if (!clientId) return;
|
|
607
|
+
for (const [requestId, pending] of pendingReadinessProbeRequests) {
|
|
608
|
+
if (!pending) continue;
|
|
609
|
+
if (
|
|
610
|
+
pending.requesterClientId === clientId ||
|
|
611
|
+
pending.targetClientId === clientId
|
|
612
|
+
) {
|
|
613
|
+
pendingReadinessProbeRequests.delete(requestId);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function buildReadinessClientEntry(clientId) {
|
|
619
|
+
const protocol = protocolState.get(clientId);
|
|
620
|
+
return {
|
|
621
|
+
clientId,
|
|
622
|
+
clientKind: classifyClientKind(protocol),
|
|
623
|
+
clientName: protocol && protocol.clientName ? protocol.clientName : null,
|
|
624
|
+
clientVersion: protocol && protocol.clientVersion ? protocol.clientVersion : null,
|
|
625
|
+
protocolSessionKey: protocol && protocol.sessionKey ? protocol.sessionKey : null,
|
|
626
|
+
readinessSnapshot: cloneReadinessSnapshot(
|
|
627
|
+
clientReadinessSnapshotState.get(clientId) || null,
|
|
628
|
+
),
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
452
632
|
function clearClientNudgeTimer(state, key, clearFn = clearTimeout) {
|
|
453
633
|
if (!state || !state[key]) return;
|
|
454
634
|
clearFn(state[key]);
|
|
@@ -888,6 +1068,50 @@ function createDownstreamServer(opts) {
|
|
|
888
1068
|
ws.send(message);
|
|
889
1069
|
}
|
|
890
1070
|
|
|
1071
|
+
function forwardReadinessProbeAck(clientId, ack) {
|
|
1072
|
+
if (!ack || !ack.requestId) {
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
const pending = pendingReadinessProbeRequests.get(ack.requestId);
|
|
1076
|
+
if (!pending || pending.targetClientId !== clientId) {
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
pendingReadinessProbeRequests.delete(ack.requestId);
|
|
1080
|
+
const requesterWs = clients.get(pending.requesterClientId);
|
|
1081
|
+
if (!requesterWs || requesterWs.readyState !== WebSocket.OPEN) {
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
const protocol = protocolState.get(clientId);
|
|
1085
|
+
const message =
|
|
1086
|
+
handler && typeof handler.formatReadinessProbeAck === "function"
|
|
1087
|
+
? handler.formatReadinessProbeAck({
|
|
1088
|
+
ok: ack.ok !== false,
|
|
1089
|
+
requestId: ack.requestId,
|
|
1090
|
+
reasonCode: ack.reasonCode || null,
|
|
1091
|
+
message: ack.message || null,
|
|
1092
|
+
activeSessionKey: ack.activeSessionKey || null,
|
|
1093
|
+
emittedAtMs: ack.emittedAtMs,
|
|
1094
|
+
clientId,
|
|
1095
|
+
clientName: protocol && protocol.clientName ? protocol.clientName : null,
|
|
1096
|
+
clientVersion:
|
|
1097
|
+
protocol && protocol.clientVersion ? protocol.clientVersion : null,
|
|
1098
|
+
})
|
|
1099
|
+
: JSON.stringify({
|
|
1100
|
+
type: APP_PROTOCOL.readinessProbeAck,
|
|
1101
|
+
ok: ack.ok !== false,
|
|
1102
|
+
requestId: ack.requestId,
|
|
1103
|
+
reasonCode: ack.reasonCode || null,
|
|
1104
|
+
message: ack.message || null,
|
|
1105
|
+
activeSessionKey: ack.activeSessionKey || null,
|
|
1106
|
+
emittedAtMs: ack.emittedAtMs,
|
|
1107
|
+
clientId,
|
|
1108
|
+
clientName: protocol && protocol.clientName ? protocol.clientName : null,
|
|
1109
|
+
clientVersion:
|
|
1110
|
+
protocol && protocol.clientVersion ? protocol.clientVersion : null,
|
|
1111
|
+
});
|
|
1112
|
+
sendMessageToClient(pending.requesterClientId, requesterWs, message);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
891
1115
|
function getServerResumeState() {
|
|
892
1116
|
if (!getCurrentResumeState) {
|
|
893
1117
|
return { pagesRevision: null, statusRevision: null };
|
|
@@ -1191,6 +1415,9 @@ function createDownstreamServer(opts) {
|
|
|
1191
1415
|
clientVersion: protocolHello.clientVersion,
|
|
1192
1416
|
sessionKey: protocolHello.sessionKey,
|
|
1193
1417
|
});
|
|
1418
|
+
if (protocolHello.readinessSnapshot) {
|
|
1419
|
+
updateClientReadinessSnapshot(clientId, protocolHello.readinessSnapshot);
|
|
1420
|
+
}
|
|
1194
1421
|
logger.info(
|
|
1195
1422
|
`[downstream] ${clientId} identified protocol=${state.selectedVersion || "n/a"} client=${describeProtocolClient(state)} kind=${classifyClientKind(state)} session=${state.sessionKey || "n/a"}`,
|
|
1196
1423
|
);
|
|
@@ -1244,6 +1471,21 @@ function createDownstreamServer(opts) {
|
|
|
1244
1471
|
return;
|
|
1245
1472
|
}
|
|
1246
1473
|
|
|
1474
|
+
if (parsed && parsed.type === APP_PROTOCOL.readinessSnapshot) {
|
|
1475
|
+
if (classifyClientKind(state) === "app") {
|
|
1476
|
+
updateClientReadinessSnapshot(clientId, parsed);
|
|
1477
|
+
}
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
const readinessProbeAck = parseReadinessProbeAck(parsed);
|
|
1482
|
+
if (readinessProbeAck) {
|
|
1483
|
+
if (classifyClientKind(state) === "app") {
|
|
1484
|
+
forwardReadinessProbeAck(clientId, readinessProbeAck);
|
|
1485
|
+
}
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1247
1489
|
if (parsed && parsed.type === APP_PROTOCOL.resume) {
|
|
1248
1490
|
const sync = syncState.get(clientId);
|
|
1249
1491
|
if (sync) sync.resumeReceived = true;
|
|
@@ -1307,6 +1549,8 @@ function createDownstreamServer(opts) {
|
|
|
1307
1549
|
const protocol = protocolState.get(clientId);
|
|
1308
1550
|
syncState.delete(clientId);
|
|
1309
1551
|
protocolState.delete(clientId);
|
|
1552
|
+
clientReadinessSnapshotState.delete(clientId);
|
|
1553
|
+
clearPendingReadinessProbesForClient(clientId);
|
|
1310
1554
|
clearClientNudgeTimers(clientId);
|
|
1311
1555
|
clientNudgeState.delete(clientId);
|
|
1312
1556
|
handler.removeClient(clientId);
|
|
@@ -1379,6 +1623,54 @@ function createDownstreamServer(opts) {
|
|
|
1379
1623
|
}
|
|
1380
1624
|
}
|
|
1381
1625
|
|
|
1626
|
+
if (result.readinessProbe) {
|
|
1627
|
+
const requestId = parseOptionalTrimmedString(
|
|
1628
|
+
result.readinessProbe.requestId,
|
|
1629
|
+
);
|
|
1630
|
+
const targetClientId = parseOptionalTrimmedString(
|
|
1631
|
+
result.readinessProbe.targetClientId,
|
|
1632
|
+
);
|
|
1633
|
+
const message =
|
|
1634
|
+
typeof result.readinessProbe.message === "string"
|
|
1635
|
+
? result.readinessProbe.message
|
|
1636
|
+
: null;
|
|
1637
|
+
const targetWs = targetClientId ? clients.get(targetClientId) : null;
|
|
1638
|
+
if (
|
|
1639
|
+
!requestId ||
|
|
1640
|
+
!targetClientId ||
|
|
1641
|
+
!message ||
|
|
1642
|
+
!targetWs ||
|
|
1643
|
+
targetWs.readyState !== WebSocket.OPEN ||
|
|
1644
|
+
!isAppClient(targetClientId)
|
|
1645
|
+
) {
|
|
1646
|
+
const requesterWs = clients.get(senderId);
|
|
1647
|
+
if (
|
|
1648
|
+
requesterWs &&
|
|
1649
|
+
requesterWs.readyState === WebSocket.OPEN &&
|
|
1650
|
+
handler &&
|
|
1651
|
+
typeof handler.formatReadinessProbeAck === "function"
|
|
1652
|
+
) {
|
|
1653
|
+
sendMessageToClient(
|
|
1654
|
+
senderId,
|
|
1655
|
+
requesterWs,
|
|
1656
|
+
handler.formatReadinessProbeAck({
|
|
1657
|
+
ok: false,
|
|
1658
|
+
requestId,
|
|
1659
|
+
reasonCode: "no_downstream_client",
|
|
1660
|
+
message: "No downstream app client connected",
|
|
1661
|
+
}),
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1664
|
+
} else {
|
|
1665
|
+
pendingReadinessProbeRequests.set(requestId, {
|
|
1666
|
+
requesterClientId: senderId,
|
|
1667
|
+
targetClientId,
|
|
1668
|
+
createdAtMs: Date.now(),
|
|
1669
|
+
});
|
|
1670
|
+
sendMessageToClient(targetClientId, targetWs, message);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1382
1674
|
if (result.broadcast) {
|
|
1383
1675
|
if (Array.isArray(result.broadcast)) {
|
|
1384
1676
|
for (const msg of result.broadcast) broadcast(msg);
|
|
@@ -1519,6 +1811,37 @@ function createDownstreamServer(opts) {
|
|
|
1519
1811
|
return countConnectedAppClients(excludeClientId);
|
|
1520
1812
|
}
|
|
1521
1813
|
|
|
1814
|
+
function getReadinessSnapshot() {
|
|
1815
|
+
const clientsOut = [];
|
|
1816
|
+
let latestUpdatedAtMs = null;
|
|
1817
|
+
for (const [clientId, ws] of clients) {
|
|
1818
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
1819
|
+
continue;
|
|
1820
|
+
}
|
|
1821
|
+
if (!isAppClient(clientId)) {
|
|
1822
|
+
continue;
|
|
1823
|
+
}
|
|
1824
|
+
const entry = buildReadinessClientEntry(clientId);
|
|
1825
|
+
clientsOut.push(entry);
|
|
1826
|
+
const updatedAtMs =
|
|
1827
|
+
entry &&
|
|
1828
|
+
entry.readinessSnapshot &&
|
|
1829
|
+
Number.isFinite(entry.readinessSnapshot.updatedAtMs)
|
|
1830
|
+
? entry.readinessSnapshot.updatedAtMs
|
|
1831
|
+
: null;
|
|
1832
|
+
if (updatedAtMs !== null && (latestUpdatedAtMs === null || updatedAtMs > latestUpdatedAtMs)) {
|
|
1833
|
+
latestUpdatedAtMs = updatedAtMs;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
clientsOut.sort((left, right) => String(left.clientId).localeCompare(String(right.clientId)));
|
|
1837
|
+
return {
|
|
1838
|
+
connectedClientCount: clientsOut.length,
|
|
1839
|
+
fanoutRecipientCount: clientsOut.length,
|
|
1840
|
+
updatedAtMs: latestUpdatedAtMs,
|
|
1841
|
+
clients: clientsOut,
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1522
1845
|
/**
|
|
1523
1846
|
* Shut down the WebSocket server and disconnect all clients.
|
|
1524
1847
|
*
|
|
@@ -1534,6 +1857,8 @@ function createDownstreamServer(opts) {
|
|
|
1534
1857
|
clearClientNudgeTimers(clientId);
|
|
1535
1858
|
}
|
|
1536
1859
|
clientNudgeState.clear();
|
|
1860
|
+
clientReadinessSnapshotState.clear();
|
|
1861
|
+
pendingReadinessProbeRequests.clear();
|
|
1537
1862
|
for (const [, ws] of clients) {
|
|
1538
1863
|
ws.close();
|
|
1539
1864
|
}
|
|
@@ -1547,6 +1872,7 @@ function createDownstreamServer(opts) {
|
|
|
1547
1872
|
unicast,
|
|
1548
1873
|
getClientIds,
|
|
1549
1874
|
getConnectedAppCount,
|
|
1875
|
+
getReadinessSnapshot,
|
|
1550
1876
|
closeConnectedAppClients,
|
|
1551
1877
|
getClientNudgeState(clientId) {
|
|
1552
1878
|
return cloneClientNudgeState(clientNudgeState.get(clientId) || null);
|
|
@@ -371,8 +371,9 @@ function createRelay(opts) {
|
|
|
371
371
|
* @param {object} context
|
|
372
372
|
* @param {() => object} buildData
|
|
373
373
|
*/
|
|
374
|
-
function emitDebug(cat, event, severity, context, buildData) {
|
|
375
|
-
|
|
374
|
+
function emitDebug(cat, event, severity, context, buildData, options) {
|
|
375
|
+
const force = !!(options && options.force === true);
|
|
376
|
+
if (!force && !debugStore.isEnabled(cat)) return;
|
|
376
377
|
|
|
377
378
|
let data = {};
|
|
378
379
|
if (typeof buildData === "function") {
|
|
@@ -394,7 +395,15 @@ function createRelay(opts) {
|
|
|
394
395
|
if (context && context.runId) payload.runId = context.runId;
|
|
395
396
|
if (context && context.screen) payload.screen = context.screen;
|
|
396
397
|
|
|
397
|
-
debugStore.emit(payload);
|
|
398
|
+
debugStore.emit(payload, { force });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function isForcedReadinessProofEvent(payload) {
|
|
402
|
+
return !!(
|
|
403
|
+
payload &&
|
|
404
|
+
payload.cat === "app.lifecycle" &&
|
|
405
|
+
payload.event === "readiness_probe_received"
|
|
406
|
+
);
|
|
398
407
|
}
|
|
399
408
|
|
|
400
409
|
function scheduleSimulateStreamTimer(delayMs, callback) {
|
|
@@ -879,6 +888,20 @@ function createRelay(opts) {
|
|
|
879
888
|
getAgentName() {
|
|
880
889
|
return upstreamRuntime ? upstreamRuntime.getAgentName() : null;
|
|
881
890
|
},
|
|
891
|
+
isPinnedFirstUserMessageKey(sessionKey) {
|
|
892
|
+
const normalizedSessionKey = normalizeEvenAiSessionKeyForLookup(sessionKey);
|
|
893
|
+
if (!normalizedSessionKey) {
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
const trackedThrowawayKeys =
|
|
897
|
+
typeof evenAiSettingsStore.getTrackedThrowawayKeys === "function"
|
|
898
|
+
? evenAiSettingsStore.getTrackedThrowawayKeys()
|
|
899
|
+
: [];
|
|
900
|
+
return dedupeNormalizedSessionKeys(trackedThrowawayKeys).some(
|
|
901
|
+
(trackedKey) =>
|
|
902
|
+
trackedKey.toLowerCase() === normalizedSessionKey.toLowerCase(),
|
|
903
|
+
);
|
|
904
|
+
},
|
|
882
905
|
onSessionStateReset: resetActivityStatusAdapter,
|
|
883
906
|
onPagesChanged: cachePages,
|
|
884
907
|
onStatusChanged: broadcastStatus,
|
|
@@ -1324,7 +1347,7 @@ function createRelay(opts) {
|
|
|
1324
1347
|
},
|
|
1325
1348
|
|
|
1326
1349
|
onGetStatus() {
|
|
1327
|
-
return buildStatusObject();
|
|
1350
|
+
return buildStatusObject({ includeDownstreamReadiness: true });
|
|
1328
1351
|
},
|
|
1329
1352
|
|
|
1330
1353
|
onGetSessionModelConfig() {
|
|
@@ -1459,7 +1482,8 @@ function createRelay(opts) {
|
|
|
1459
1482
|
onEventDebug(clientId, payload) {
|
|
1460
1483
|
if (!payload || typeof payload !== "object") return;
|
|
1461
1484
|
const cat = payload.cat;
|
|
1462
|
-
|
|
1485
|
+
const forceStore = isForcedReadinessProofEvent(payload);
|
|
1486
|
+
if (!forceStore && !debugStore.isEnabled(cat)) return;
|
|
1463
1487
|
emitDebug(
|
|
1464
1488
|
cat,
|
|
1465
1489
|
payload.event,
|
|
@@ -1473,6 +1497,7 @@ function createRelay(opts) {
|
|
|
1473
1497
|
clientId,
|
|
1474
1498
|
...(payload.data || {}),
|
|
1475
1499
|
}),
|
|
1500
|
+
{ force: forceStore },
|
|
1476
1501
|
);
|
|
1477
1502
|
},
|
|
1478
1503
|
|
|
@@ -1637,6 +1662,110 @@ function createRelay(opts) {
|
|
|
1637
1662
|
control,
|
|
1638
1663
|
};
|
|
1639
1664
|
},
|
|
1665
|
+
|
|
1666
|
+
onReadinessProbe(clientId, request) {
|
|
1667
|
+
const now = Date.now();
|
|
1668
|
+
const requestId =
|
|
1669
|
+
(typeof request.requestId === "string" && request.requestId.trim()) ||
|
|
1670
|
+
`readiness-${now}-${Math.random().toString(16).slice(2, 8)}`;
|
|
1671
|
+
const sinceMs = Number.isFinite(Number(request && request.sinceMs))
|
|
1672
|
+
? Math.max(0, Math.floor(Number(request.sinceMs)))
|
|
1673
|
+
: now;
|
|
1674
|
+
const snapshot =
|
|
1675
|
+
server && typeof server.getReadinessSnapshot === "function"
|
|
1676
|
+
? server.getReadinessSnapshot()
|
|
1677
|
+
: {
|
|
1678
|
+
connectedClientCount: 0,
|
|
1679
|
+
fanoutRecipientCount: 0,
|
|
1680
|
+
clients: [],
|
|
1681
|
+
};
|
|
1682
|
+
const targetClientId =
|
|
1683
|
+
snapshot &&
|
|
1684
|
+
snapshot.connectedClientCount === 1 &&
|
|
1685
|
+
snapshot.fanoutRecipientCount === 1 &&
|
|
1686
|
+
Array.isArray(snapshot.clients) &&
|
|
1687
|
+
snapshot.clients.length === 1 &&
|
|
1688
|
+
typeof snapshot.clients[0].clientId === "string"
|
|
1689
|
+
? snapshot.clients[0].clientId
|
|
1690
|
+
: null;
|
|
1691
|
+
|
|
1692
|
+
emitDebug(
|
|
1693
|
+
"relay.protocol",
|
|
1694
|
+
"readiness_probe_requested",
|
|
1695
|
+
"info",
|
|
1696
|
+
{ sessionKey: sessionService.ensureSessionKey() },
|
|
1697
|
+
() => ({
|
|
1698
|
+
clientId,
|
|
1699
|
+
requestId,
|
|
1700
|
+
sinceMs,
|
|
1701
|
+
requestedSessionKey:
|
|
1702
|
+
typeof request.sessionKey === "string" && request.sessionKey.trim()
|
|
1703
|
+
? request.sessionKey.trim()
|
|
1704
|
+
: null,
|
|
1705
|
+
connectedClientCount:
|
|
1706
|
+
snapshot && Number.isFinite(snapshot.connectedClientCount)
|
|
1707
|
+
? snapshot.connectedClientCount
|
|
1708
|
+
: 0,
|
|
1709
|
+
fanoutRecipientCount:
|
|
1710
|
+
snapshot && Number.isFinite(snapshot.fanoutRecipientCount)
|
|
1711
|
+
? snapshot.fanoutRecipientCount
|
|
1712
|
+
: 0,
|
|
1713
|
+
}),
|
|
1714
|
+
);
|
|
1715
|
+
|
|
1716
|
+
if (
|
|
1717
|
+
!snapshot ||
|
|
1718
|
+
snapshot.connectedClientCount <= 0 ||
|
|
1719
|
+
snapshot.fanoutRecipientCount <= 0
|
|
1720
|
+
) {
|
|
1721
|
+
return {
|
|
1722
|
+
ok: false,
|
|
1723
|
+
requestId,
|
|
1724
|
+
reasonCode: "no_downstream_client",
|
|
1725
|
+
message: "No downstream app clients connected",
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
if (
|
|
1730
|
+
snapshot.connectedClientCount > 1 ||
|
|
1731
|
+
snapshot.fanoutRecipientCount > 1 ||
|
|
1732
|
+
!targetClientId
|
|
1733
|
+
) {
|
|
1734
|
+
return {
|
|
1735
|
+
ok: false,
|
|
1736
|
+
requestId,
|
|
1737
|
+
reasonCode: "multi_recipient_fanout",
|
|
1738
|
+
message: "Multiple downstream app clients connected",
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
emitDebug(
|
|
1743
|
+
"relay.protocol",
|
|
1744
|
+
"readiness_probe_dispatched",
|
|
1745
|
+
"info",
|
|
1746
|
+
{ sessionKey: sessionService.ensureSessionKey() },
|
|
1747
|
+
() => ({
|
|
1748
|
+
clientId,
|
|
1749
|
+
requestId,
|
|
1750
|
+
targetClientId,
|
|
1751
|
+
sinceMs,
|
|
1752
|
+
}),
|
|
1753
|
+
);
|
|
1754
|
+
|
|
1755
|
+
return {
|
|
1756
|
+
ok: true,
|
|
1757
|
+
requestId,
|
|
1758
|
+
targetClientId,
|
|
1759
|
+
probe: {
|
|
1760
|
+
requestId,
|
|
1761
|
+
sinceMs,
|
|
1762
|
+
sessionKey:
|
|
1763
|
+
typeof request.sessionKey === "string" && request.sessionKey.trim()
|
|
1764
|
+
? request.sessionKey.trim()
|
|
1765
|
+
: null,
|
|
1766
|
+
},
|
|
1767
|
+
};
|
|
1768
|
+
},
|
|
1640
1769
|
});
|
|
1641
1770
|
|
|
1642
1771
|
// --- Downstream server ---
|
|
@@ -1739,8 +1868,9 @@ function createRelay(opts) {
|
|
|
1739
1868
|
|
|
1740
1869
|
// --- Helpers ---
|
|
1741
1870
|
|
|
1742
|
-
function buildStatusObject() {
|
|
1743
|
-
|
|
1871
|
+
function buildStatusObject(options = {}) {
|
|
1872
|
+
const includeDownstreamReadiness = options.includeDownstreamReadiness === true;
|
|
1873
|
+
const status = {
|
|
1744
1874
|
openclaw:
|
|
1745
1875
|
upstreamRuntime && upstreamRuntime.isConnected()
|
|
1746
1876
|
? "connected"
|
|
@@ -1749,6 +1879,18 @@ function createRelay(opts) {
|
|
|
1749
1879
|
session: sessionService.ensureSessionKey(),
|
|
1750
1880
|
evenAiEnabled: opts.evenAiEnabled === true,
|
|
1751
1881
|
};
|
|
1882
|
+
if (includeDownstreamReadiness) {
|
|
1883
|
+
status.downstreamReadiness =
|
|
1884
|
+
server && typeof server.getReadinessSnapshot === "function"
|
|
1885
|
+
? server.getReadinessSnapshot()
|
|
1886
|
+
: {
|
|
1887
|
+
connectedClientCount: 0,
|
|
1888
|
+
fanoutRecipientCount: 0,
|
|
1889
|
+
updatedAtMs: null,
|
|
1890
|
+
clients: [],
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
return status;
|
|
1752
1894
|
}
|
|
1753
1895
|
|
|
1754
1896
|
function cachePages(pages) {
|
|
@@ -53,6 +53,10 @@ export function createSessionService(opts = {}) {
|
|
|
53
53
|
typeof opts.onSessionModelConfig === "function"
|
|
54
54
|
? opts.onSessionModelConfig
|
|
55
55
|
: null;
|
|
56
|
+
const isPinnedFirstUserMessageKey =
|
|
57
|
+
typeof opts.isPinnedFirstUserMessageKey === "function"
|
|
58
|
+
? opts.isPinnedFirstUserMessageKey
|
|
59
|
+
: null;
|
|
56
60
|
|
|
57
61
|
/** Current session key. Generated on first use. */
|
|
58
62
|
let currentSessionKey = null;
|
|
@@ -390,7 +394,7 @@ export function createSessionService(opts = {}) {
|
|
|
390
394
|
}
|
|
391
395
|
const request = { key: canonicalKey };
|
|
392
396
|
if (patch && typeof patch.modelRef === "string") {
|
|
393
|
-
request.model = patch.modelRef;
|
|
397
|
+
request.model = patch.modelRef.trim() ? patch.modelRef : null;
|
|
394
398
|
}
|
|
395
399
|
if (patch && Object.prototype.hasOwnProperty.call(patch, "thinkingLevel")) {
|
|
396
400
|
request.thinkingLevel =
|
|
@@ -717,11 +721,7 @@ export function createSessionService(opts = {}) {
|
|
|
717
721
|
if (!sessionKey || !normalized) continue;
|
|
718
722
|
out.set(sessionKey, normalized);
|
|
719
723
|
}
|
|
720
|
-
|
|
721
|
-
const oldestKey = out.keys().next().value;
|
|
722
|
-
if (oldestKey === undefined) break;
|
|
723
|
-
out.delete(oldestKey);
|
|
724
|
-
}
|
|
724
|
+
pruneFirstUserMessageEntries(out);
|
|
725
725
|
return out;
|
|
726
726
|
} catch {
|
|
727
727
|
return new Map();
|
|
@@ -756,11 +756,7 @@ export function createSessionService(opts = {}) {
|
|
|
756
756
|
}
|
|
757
757
|
|
|
758
758
|
function pruneFirstSentUserMessageCache() {
|
|
759
|
-
|
|
760
|
-
const oldestKey = firstSentUserMessageBySession.keys().next().value;
|
|
761
|
-
if (oldestKey === undefined) break;
|
|
762
|
-
firstSentUserMessageBySession.delete(oldestKey);
|
|
763
|
-
}
|
|
759
|
+
pruneFirstUserMessageEntries(firstSentUserMessageBySession);
|
|
764
760
|
}
|
|
765
761
|
|
|
766
762
|
function recordFirstSentUserMessage(sessionKey, text) {
|
|
@@ -810,10 +806,41 @@ export function createSessionService(opts = {}) {
|
|
|
810
806
|
}
|
|
811
807
|
|
|
812
808
|
function pruneFirstUserMessageCache() {
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
809
|
+
pruneFirstUserMessageEntries(firstUserMessageCache);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function shouldPinFirstUserMessageKey(sessionKey) {
|
|
813
|
+
if (!isPinnedFirstUserMessageKey || typeof sessionKey !== "string") {
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
const normalizedKey = sessionKey.trim();
|
|
817
|
+
if (!normalizedKey) {
|
|
818
|
+
return false;
|
|
819
|
+
}
|
|
820
|
+
try {
|
|
821
|
+
return isPinnedFirstUserMessageKey(normalizedKey) === true;
|
|
822
|
+
} catch (err) {
|
|
823
|
+
logger.warn(
|
|
824
|
+
`[relay] first-user cache pin callback failed for ${normalizedKey}: ${err && err.message ? err.message : err}`,
|
|
825
|
+
);
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function pruneFirstUserMessageEntries(cache) {
|
|
831
|
+
while (cache.size > firstUserMessageCacheLimit) {
|
|
832
|
+
let evicted = false;
|
|
833
|
+
for (const sessionKey of cache.keys()) {
|
|
834
|
+
if (shouldPinFirstUserMessageKey(sessionKey)) {
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
cache.delete(sessionKey);
|
|
838
|
+
evicted = true;
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
if (!evicted) {
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
817
844
|
}
|
|
818
845
|
}
|
|
819
846
|
|