ocuclaw 0.1.0 → 1.3.0
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 +63 -8
- package/dist/config/runtime-config.js +81 -3
- package/dist/domain/activity-status-adapter.js +138 -605
- package/dist/domain/activity-status-arbiter.js +109 -0
- package/dist/domain/activity-status-labels.js +906 -0
- package/dist/domain/code-span-regions.js +103 -0
- package/dist/domain/conversation-state.js +14 -1
- package/dist/domain/debug-store.js +41 -184
- package/dist/domain/glasses-ui-content-summary.js +62 -0
- package/dist/domain/glasses-ui-system-prompt.js +28 -0
- package/dist/domain/message-emoji-allowlist.js +16 -0
- package/dist/domain/message-emoji-filter.js +33 -55
- package/dist/domain/neural-emoji-reactor-system-prompt.js +43 -0
- package/dist/domain/neural-emoji-reactor-tag-config.js +56 -0
- package/dist/domain/neural-pace-modulator-system-prompt.js +32 -0
- package/dist/domain/neural-pace-modulator-tag-config.js +51 -0
- package/dist/domain/tagged-span-parser.js +121 -0
- package/dist/domain/tagged-span-strip.js +38 -0
- package/dist/even-ai/even-ai-endpoint.js +91 -0
- package/dist/even-ai/even-ai-run-waiter.js +14 -0
- package/dist/even-ai/even-ai-settings-store.js +14 -0
- package/dist/gateway/gateway-bridge.js +14 -2
- package/dist/gateway/gateway-timing-ledger.js +457 -0
- package/dist/gateway/openclaw-client.js +462 -38
- package/dist/index.js +28 -1
- package/dist/runtime/downstream-handler.js +909 -68
- package/dist/runtime/downstream-server.js +1004 -512
- package/dist/runtime/ocuclaw-settings-store.js +74 -31
- package/dist/runtime/plugin-update-service.js +216 -0
- package/dist/runtime/protocol-adapter.js +9 -0
- package/dist/runtime/provider-usage-select.js +168 -0
- package/dist/runtime/relay-client-nudge-controller.js +553 -0
- package/dist/runtime/relay-core.js +1357 -210
- package/dist/runtime/relay-health-monitor.js +172 -0
- package/dist/runtime/relay-operation-registry.js +263 -0
- package/dist/runtime/relay-service.js +201 -1
- package/dist/runtime/relay-worker-approval-replay-cache.js +68 -0
- package/dist/runtime/relay-worker-entry.js +32 -0
- package/dist/runtime/relay-worker-health.js +272 -0
- package/dist/runtime/relay-worker-protocol.js +285 -0
- package/dist/runtime/relay-worker-queue.js +202 -0
- package/dist/runtime/relay-worker-supervisor.js +1081 -0
- package/dist/runtime/relay-worker-transport.js +1051 -0
- package/dist/runtime/session-context-service.js +189 -0
- package/dist/runtime/session-service.js +656 -38
- package/dist/runtime/upstream-runtime.js +1167 -60
- package/dist/tools/device-info-tool.js +242 -0
- package/dist/tools/glasses-ui-cron.js +427 -0
- package/dist/tools/glasses-ui-descriptors.js +261 -0
- package/dist/tools/glasses-ui-limits.js +21 -0
- package/dist/tools/glasses-ui-paint-floor.js +99 -0
- package/dist/tools/glasses-ui-recipes.js +746 -0
- package/dist/tools/glasses-ui-surfaces.js +278 -0
- package/dist/tools/glasses-ui-template.js +182 -0
- package/dist/tools/glasses-ui-tool.js +1147 -0
- package/dist/tools/session-title-tool.js +209 -0
- package/dist/version.js +2 -0
- package/openclaw.plugin.json +163 -15
- package/package.json +12 -4
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import * as http from "node:http";
|
|
3
2
|
import * as path from "node:path";
|
|
3
|
+
import * as childProcess from "node:child_process";
|
|
4
|
+
import { EventEmitter } from "node:events";
|
|
5
|
+
import { createPluginUpdateService } from "./plugin-update-service.js";
|
|
4
6
|
import * as conversationStateModule from "../domain/conversation-state.js";
|
|
5
7
|
import { createDebugStore } from "../domain/debug-store.js";
|
|
8
|
+
import { summarizeGlassesUiContent } from "../domain/glasses-ui-content-summary.js";
|
|
6
9
|
import { composeReadabilitySystemPrompt } from "../domain/readability-system-prompt.js";
|
|
10
|
+
import { composeNeuralEmojiReactorSystemPrompt } from "../domain/neural-emoji-reactor-system-prompt.js";
|
|
11
|
+
import { composeNeuralPaceModulatorSystemPrompt } from "../domain/neural-pace-modulator-system-prompt.js";
|
|
12
|
+
import { composeGlassesUiNudgeSystemPrompt } from "../domain/glasses-ui-system-prompt.js";
|
|
7
13
|
import { createActivityStatusAdapter } from "../domain/activity-status-adapter.js";
|
|
8
14
|
import { createEvenAiEndpoint } from "../even-ai/even-ai-endpoint.js";
|
|
9
15
|
import { createEvenAiRouter } from "../even-ai/even-ai-router.js";
|
|
@@ -12,15 +18,20 @@ import { createEvenAiSettingsStore } from "../even-ai/even-ai-settings-store.js"
|
|
|
12
18
|
import { createPluginOpenclawClient } from "../gateway/openclaw-client.js";
|
|
13
19
|
import { createPluginRpcGatewayBridge } from "../gateway/gateway-bridge.js";
|
|
14
20
|
import { createDownstreamHandler } from "./downstream-handler.js";
|
|
15
|
-
import { createDownstreamServer } from "./downstream-server.js";
|
|
16
21
|
import { createOcuClawSettingsStore } from "./ocuclaw-settings-store.js";
|
|
22
|
+
import { createRelayHealthMonitor } from "./relay-health-monitor.js";
|
|
23
|
+
import { createRelayOperationRegistry } from "./relay-operation-registry.js";
|
|
24
|
+
import { createRelayWorkerSupervisor } from "./relay-worker-supervisor.js";
|
|
17
25
|
import { createSessionService } from "./session-service.js";
|
|
18
26
|
import { createUpstreamRuntime } from "./upstream-runtime.js";
|
|
19
27
|
|
|
20
28
|
const SONIOX_TEMP_KEY_URL = "https://api.soniox.com/v1/auth/temporary-api-key";
|
|
21
29
|
const SONIOX_MODELS_URL = "https://api.soniox.com/v1/models";
|
|
22
30
|
const DEFAULT_SONIOX_TEMP_KEY_EXPIRES_IN_SECONDS = 3600;
|
|
23
|
-
|
|
31
|
+
// Maximum time (ms) to wait for a Soniox temp-key mint before aborting the
|
|
32
|
+
// fetch. 8 s is a conservative cold-path ceiling; tests inject a tiny value
|
|
33
|
+
// via opts.sonioxTemporaryKeyMintTimeoutMs to make assertions fast.
|
|
34
|
+
const DEFAULT_SONIOX_TEMP_KEY_MINT_TIMEOUT_MS = 8000;
|
|
24
35
|
const EVEN_AI_NAMESPACE_PREFIX = "ocuclaw:even-ai";
|
|
25
36
|
const EVEN_AI_NAMESPACE_PREFIX_WITH_DELIMITER = "ocuclaw:even-ai:";
|
|
26
37
|
const LISTEN_INTERCEPT_RECOVERY_ERROR = "Voice interrupted; retry";
|
|
@@ -159,6 +170,10 @@ function normalizeSonioxTemporaryKeyErrorCode(err) {
|
|
|
159
170
|
: "";
|
|
160
171
|
const lowered = message.toLowerCase();
|
|
161
172
|
if (!message) return "soniox_temp_key_request_failed";
|
|
173
|
+
// AbortError from the per-fetch timeout AbortController.
|
|
174
|
+
if (err && err.name === "AbortError") {
|
|
175
|
+
return "soniox_temp_key_mint_timeout";
|
|
176
|
+
}
|
|
162
177
|
if (lowered.includes("api key is not configured")) {
|
|
163
178
|
return "soniox_temp_key_not_configured";
|
|
164
179
|
}
|
|
@@ -210,31 +225,71 @@ function normalizeSonioxModelEntryRows(result) {
|
|
|
210
225
|
return models;
|
|
211
226
|
}
|
|
212
227
|
|
|
213
|
-
function
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
228
|
+
function createBufferedHttpRequest(envelope) {
|
|
229
|
+
const req = new EventEmitter();
|
|
230
|
+
req.method = envelope && envelope.method ? envelope.method : "GET";
|
|
231
|
+
req.url = envelope && envelope.url ? envelope.url : "/";
|
|
232
|
+
req.headers = envelope && envelope.headers && typeof envelope.headers === "object"
|
|
233
|
+
? envelope.headers
|
|
234
|
+
: {};
|
|
235
|
+
req.socket = {
|
|
236
|
+
remoteAddress: "127.0.0.1",
|
|
237
|
+
};
|
|
238
|
+
const body = Buffer.from((envelope && envelope.bodyBase64) || "", "base64");
|
|
239
|
+
process.nextTick(() => {
|
|
240
|
+
if (body.length > 0) {
|
|
241
|
+
req.emit("data", body);
|
|
217
242
|
}
|
|
218
|
-
|
|
219
|
-
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
220
|
-
res.end("not found");
|
|
243
|
+
req.emit("end");
|
|
221
244
|
});
|
|
245
|
+
return req;
|
|
222
246
|
}
|
|
223
247
|
|
|
224
|
-
function
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
248
|
+
function createBufferedHttpResponse(maxResponseBytes) {
|
|
249
|
+
const headers = {};
|
|
250
|
+
const chunks = [];
|
|
251
|
+
let totalBytes = 0;
|
|
252
|
+
const limit = Number.isFinite(maxResponseBytes) && maxResponseBytes > 0
|
|
253
|
+
? Math.floor(maxResponseBytes)
|
|
254
|
+
: 262_144;
|
|
255
|
+
// EventEmitter shape: handlers like the Even-AI endpoint subscribe to
|
|
256
|
+
// res.once('close', ...) for client-disconnect detection. Worker-mode
|
|
257
|
+
// relays actual client closes through an http.cancel worker message.
|
|
258
|
+
const res = new EventEmitter();
|
|
259
|
+
res.statusCode = 200;
|
|
260
|
+
res.writableEnded = false;
|
|
261
|
+
res.setHeader = function (name, value) {
|
|
262
|
+
if (typeof name === "string" && name) {
|
|
263
|
+
headers[name.toLowerCase()] = value;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
res.getHeader = function (name) {
|
|
267
|
+
return typeof name === "string" ? headers[name.toLowerCase()] : undefined;
|
|
268
|
+
};
|
|
269
|
+
res.write = function (chunk) {
|
|
270
|
+
if (this.writableEnded) return false;
|
|
271
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk ?? ""));
|
|
272
|
+
totalBytes += buffer.length;
|
|
273
|
+
if (totalBytes > limit) {
|
|
274
|
+
throw new Error("Buffered HTTP response exceeded relay worker limit");
|
|
275
|
+
}
|
|
276
|
+
chunks.push(buffer);
|
|
277
|
+
return true;
|
|
278
|
+
};
|
|
279
|
+
res.end = function (chunk) {
|
|
280
|
+
if (chunk !== undefined && chunk !== null) {
|
|
281
|
+
this.write(chunk);
|
|
282
|
+
}
|
|
283
|
+
this.writableEnded = true;
|
|
284
|
+
};
|
|
285
|
+
res.toResult = function () {
|
|
286
|
+
return {
|
|
287
|
+
statusCode: this.statusCode,
|
|
288
|
+
headers: { ...headers },
|
|
289
|
+
body: Buffer.concat(chunks),
|
|
290
|
+
};
|
|
291
|
+
};
|
|
292
|
+
return res;
|
|
238
293
|
}
|
|
239
294
|
|
|
240
295
|
// --- Factory ---
|
|
@@ -283,9 +338,7 @@ function createRelay(opts) {
|
|
|
283
338
|
const activityStatusAdapter = createActivityStatusAdapter(
|
|
284
339
|
opts.activityStatusAdapter,
|
|
285
340
|
);
|
|
286
|
-
const
|
|
287
|
-
!opts.httpServer && opts.evenAiEnabled === true ? createOwnedRelayHttpServer() : null;
|
|
288
|
-
const sharedHttpServer = opts.httpServer || ownedHttpServer || null;
|
|
341
|
+
const sharedHttpServer = opts.httpServer || null;
|
|
289
342
|
|
|
290
343
|
// --- Cached state ---
|
|
291
344
|
|
|
@@ -298,6 +351,8 @@ function createRelay(opts) {
|
|
|
298
351
|
let cachedStatus = null;
|
|
299
352
|
/** Monotonic status snapshot revision used for resume handshake. */
|
|
300
353
|
let statusRevision = 0;
|
|
354
|
+
/** @type {{sessionKey: string, modelProvider: string|null, model: string|null, thinkingLevel: string, reasoningLevel: string, verboseLevel: string}|null} */
|
|
355
|
+
let currentSessionModelConfigSnapshot = null;
|
|
301
356
|
|
|
302
357
|
/** Relay-local deterministic simulate-stream run sequence counter. */
|
|
303
358
|
let simulateStreamRunSeq = 0;
|
|
@@ -306,18 +361,48 @@ function createRelay(opts) {
|
|
|
306
361
|
|
|
307
362
|
// --- Structured debug state ---
|
|
308
363
|
|
|
364
|
+
const debugCategories = Array.isArray(opts.debugCategories)
|
|
365
|
+
? opts.debugCategories
|
|
366
|
+
: opts.debugCategories && typeof opts.debugCategories === "object"
|
|
367
|
+
? Object.entries(opts.debugCategories)
|
|
368
|
+
.filter(([, enabled]) => enabled)
|
|
369
|
+
.map(([category]) => category)
|
|
370
|
+
: opts.debugCategories;
|
|
371
|
+
// Single relay-side clock: the debug store, the emitDebug ts stamp, and the
|
|
372
|
+
// liveui log tee all read from this one source so store records and `[liveui]`
|
|
373
|
+
// log lines share an identical ts (downstream reconcilers dedupe on it).
|
|
374
|
+
const debugNow =
|
|
375
|
+
typeof opts.debugNow === "function" ? opts.debugNow : () => Date.now();
|
|
309
376
|
const debugStore = createDebugStore({
|
|
310
|
-
categories:
|
|
377
|
+
categories: debugCategories,
|
|
311
378
|
capacity: opts.debugCapacity,
|
|
312
379
|
payloadMaxBytes: opts.debugPayloadMaxBytes,
|
|
313
380
|
defaultTtlMs: opts.debugDefaultTtlMs,
|
|
314
381
|
maxTtlMs: opts.debugMaxTtlMs,
|
|
315
382
|
dumpDefaultLimit: opts.debugDumpDefaultLimit,
|
|
316
383
|
dumpMaxLimit: opts.debugDumpMaxLimit,
|
|
317
|
-
now:
|
|
384
|
+
now: debugNow,
|
|
318
385
|
noisyPolicies: opts.debugNoisyPolicies,
|
|
319
386
|
});
|
|
320
387
|
|
|
388
|
+
// --- Live-interface trace-log flag (durable across restarts) ---
|
|
389
|
+
// Gates the glasses.lifecycle → gateway-log tee. Read once at construction;
|
|
390
|
+
// toggled live by applyTraceLogSet, which also rewrites this file so the
|
|
391
|
+
// value survives a relay/gateway restart (the store's enable-state does not).
|
|
392
|
+
const liveUiTraceFlagPath =
|
|
393
|
+
typeof opts.stateDir === "string" && opts.stateDir
|
|
394
|
+
? path.join(opts.stateDir, "liveui-trace.json")
|
|
395
|
+
: null;
|
|
396
|
+
let liveUiTraceLogEnabled = false;
|
|
397
|
+
if (liveUiTraceFlagPath) {
|
|
398
|
+
try {
|
|
399
|
+
liveUiTraceLogEnabled =
|
|
400
|
+
JSON.parse(fs.readFileSync(liveUiTraceFlagPath, "utf8")).enabled === true;
|
|
401
|
+
} catch {
|
|
402
|
+
liveUiTraceLogEnabled = false;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
321
406
|
// --- Console log file ---
|
|
322
407
|
|
|
323
408
|
const consoleLogPath =
|
|
@@ -371,8 +456,11 @@ function createRelay(opts) {
|
|
|
371
456
|
* @param {object} context
|
|
372
457
|
* @param {() => object} buildData
|
|
373
458
|
*/
|
|
374
|
-
function emitDebug(cat, event, severity, context, buildData) {
|
|
375
|
-
|
|
459
|
+
function emitDebug(cat, event, severity, context, buildData, options) {
|
|
460
|
+
const force = !!(options && options.force === true);
|
|
461
|
+
if (!force && !debugStore.isEnabled(cat) && !(liveUiTraceLogEnabled && (cat === "glasses.lifecycle" || cat === "openclaw.message"))) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
376
464
|
|
|
377
465
|
let data = {};
|
|
378
466
|
if (typeof buildData === "function") {
|
|
@@ -383,18 +471,55 @@ function createRelay(opts) {
|
|
|
383
471
|
}
|
|
384
472
|
}
|
|
385
473
|
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
event,
|
|
389
|
-
severity,
|
|
390
|
-
data,
|
|
391
|
-
};
|
|
474
|
+
const ts = debugNow();
|
|
475
|
+
const payload = { ts, cat, event, severity, data };
|
|
392
476
|
|
|
393
477
|
if (context && context.sessionKey) payload.sessionKey = context.sessionKey;
|
|
394
478
|
if (context && context.runId) payload.runId = context.runId;
|
|
395
479
|
if (context && context.screen) payload.screen = context.screen;
|
|
396
480
|
|
|
397
|
-
debugStore.emit(payload);
|
|
481
|
+
debugStore.emit(payload, { force });
|
|
482
|
+
|
|
483
|
+
// Durable openclaw-side trace tee (gated by the persistent flag, NOT the
|
|
484
|
+
// store category enable). Must never throw into the emit path.
|
|
485
|
+
if (liveUiTraceLogEnabled && (cat === "glasses.lifecycle" || cat === "openclaw.message")) {
|
|
486
|
+
try {
|
|
487
|
+
const surfaceId =
|
|
488
|
+
data && typeof data.surfaceId === "string" ? data.surfaceId : null;
|
|
489
|
+
const sessionKey =
|
|
490
|
+
payload.sessionKey ||
|
|
491
|
+
(data && typeof data.sessionKey === "string" ? data.sessionKey : null) ||
|
|
492
|
+
null;
|
|
493
|
+
const side =
|
|
494
|
+
cat === "openclaw.message"
|
|
495
|
+
? (event === "user_message" ? "user" : "agent")
|
|
496
|
+
: "openclaw";
|
|
497
|
+
logger.info(
|
|
498
|
+
"[liveui] " +
|
|
499
|
+
JSON.stringify({
|
|
500
|
+
trace: "liveui",
|
|
501
|
+
side,
|
|
502
|
+
ts,
|
|
503
|
+
cat,
|
|
504
|
+
event,
|
|
505
|
+
severity,
|
|
506
|
+
surfaceId,
|
|
507
|
+
sessionKey,
|
|
508
|
+
data,
|
|
509
|
+
}),
|
|
510
|
+
);
|
|
511
|
+
} catch {
|
|
512
|
+
// observability must never break the emit path
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function isForcedReadinessProofEvent(payload) {
|
|
518
|
+
return !!(
|
|
519
|
+
payload &&
|
|
520
|
+
payload.cat === "app.lifecycle" &&
|
|
521
|
+
payload.event === "readiness_probe_received"
|
|
522
|
+
);
|
|
398
523
|
}
|
|
399
524
|
|
|
400
525
|
function scheduleSimulateStreamTimer(delayMs, callback) {
|
|
@@ -430,6 +555,11 @@ function createRelay(opts) {
|
|
|
430
555
|
)
|
|
431
556
|
? Math.max(30, Math.floor(opts.sonioxTemporaryKeyExpiresInSeconds))
|
|
432
557
|
: DEFAULT_SONIOX_TEMP_KEY_EXPIRES_IN_SECONDS;
|
|
558
|
+
const sonioxTemporaryKeyMintTimeoutMs = Number.isFinite(
|
|
559
|
+
opts.sonioxTemporaryKeyMintTimeoutMs,
|
|
560
|
+
)
|
|
561
|
+
? Math.max(1, Math.floor(opts.sonioxTemporaryKeyMintTimeoutMs))
|
|
562
|
+
: DEFAULT_SONIOX_TEMP_KEY_MINT_TIMEOUT_MS;
|
|
433
563
|
/** @type {Array<{id: string, name: string, supportsMaxEndpointDelay: boolean}>|null} */
|
|
434
564
|
let cachedSonioxModels = null;
|
|
435
565
|
let cachedSonioxModelsFetchedAt = 0;
|
|
@@ -658,18 +788,29 @@ function createRelay(opts) {
|
|
|
658
788
|
throw new Error("fetch is not available for Soniox temporary-key minting");
|
|
659
789
|
}
|
|
660
790
|
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
791
|
+
const mintAbortController = new AbortController();
|
|
792
|
+
const mintTimeoutTimer = setTimeout(
|
|
793
|
+
() => mintAbortController.abort(),
|
|
794
|
+
sonioxTemporaryKeyMintTimeoutMs,
|
|
795
|
+
);
|
|
796
|
+
let response;
|
|
797
|
+
try {
|
|
798
|
+
response = await fetchImpl(SONIOX_TEMP_KEY_URL, {
|
|
799
|
+
method: "POST",
|
|
800
|
+
headers: {
|
|
801
|
+
Authorization: `Bearer ${configuredSonioxApiKey}`,
|
|
802
|
+
"Content-Type": "application/json",
|
|
803
|
+
},
|
|
804
|
+
body: JSON.stringify({
|
|
805
|
+
usage_type: "transcribe_websocket",
|
|
806
|
+
expires_in_seconds: sonioxTemporaryKeyExpiresInSeconds,
|
|
807
|
+
client_reference_id: voiceSessionId,
|
|
808
|
+
}),
|
|
809
|
+
signal: mintAbortController.signal,
|
|
810
|
+
});
|
|
811
|
+
} finally {
|
|
812
|
+
clearTimeout(mintTimeoutTimer);
|
|
813
|
+
}
|
|
673
814
|
|
|
674
815
|
const rawText =
|
|
675
816
|
response && typeof response.text === "function"
|
|
@@ -755,11 +896,63 @@ function createRelay(opts) {
|
|
|
755
896
|
},
|
|
756
897
|
});
|
|
757
898
|
|
|
758
|
-
function currentOcuClawSendOptions() {
|
|
899
|
+
function currentOcuClawSendOptions(perTurnSignals) {
|
|
900
|
+
const signals = perTurnSignals || {};
|
|
901
|
+
const baseReadability = composeReadabilitySystemPrompt(
|
|
902
|
+
ocuClawSettingsStore.getSnapshot().systemPrompt,
|
|
903
|
+
);
|
|
904
|
+
const validState = (raw) =>
|
|
905
|
+
raw === "active" || raw === "recently-disabled" || raw === "inactive"
|
|
906
|
+
? raw
|
|
907
|
+
: "inactive";
|
|
908
|
+
const reactorState = validState(signals.neuralEmojiReactorState);
|
|
909
|
+
const paceState = validState(signals.neuralPaceModulatorState);
|
|
910
|
+
const reactor = composeNeuralEmojiReactorSystemPrompt({ state: reactorState });
|
|
911
|
+
const pace = composeNeuralPaceModulatorSystemPrompt({ state: paceState });
|
|
912
|
+
// Only include the glasses-UI nudge when a downstream app client is
|
|
913
|
+
// connected. Keeps the prompt clean when the agent has nowhere to render
|
|
914
|
+
// the tool's output, and keeps existing prompt-assembly tests stable
|
|
915
|
+
// (they exercise the prompt without spinning up an app client).
|
|
916
|
+
const hasAppClient =
|
|
917
|
+
server &&
|
|
918
|
+
typeof server.getConnectedAppCount === "function" &&
|
|
919
|
+
server.getConnectedAppCount() > 0;
|
|
920
|
+
const glassesUiNudge = hasAppClient ? composeGlassesUiNudgeSystemPrompt() : "";
|
|
921
|
+
const parts = [];
|
|
922
|
+
if (baseReadability) parts.push(baseReadability);
|
|
923
|
+
if (reactor) parts.push(reactor);
|
|
924
|
+
if (pace) parts.push(pace);
|
|
925
|
+
if (glassesUiNudge) parts.push(glassesUiNudge);
|
|
759
926
|
return {
|
|
760
|
-
extraSystemPrompt:
|
|
761
|
-
|
|
762
|
-
|
|
927
|
+
extraSystemPrompt: parts.join("\n\n"),
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function buildOcuClawSendDiagnostic(params = {}) {
|
|
932
|
+
const attachment = params.attachment || null;
|
|
933
|
+
const messageId =
|
|
934
|
+
typeof params.id === "string" && params.id.trim()
|
|
935
|
+
? params.id.trim()
|
|
936
|
+
: null;
|
|
937
|
+
const sessionKey =
|
|
938
|
+
typeof params.sessionKey === "string" && params.sessionKey.trim()
|
|
939
|
+
? params.sessionKey.trim()
|
|
940
|
+
: sessionService.ensureSessionKey();
|
|
941
|
+
const source =
|
|
942
|
+
typeof params.source === "string" && params.source.trim()
|
|
943
|
+
? params.source.trim()
|
|
944
|
+
: "relay_send";
|
|
945
|
+
|
|
946
|
+
return {
|
|
947
|
+
messageId,
|
|
948
|
+
sessionKey,
|
|
949
|
+
source,
|
|
950
|
+
textChars: typeof params.text === "string" ? params.text.length : 0,
|
|
951
|
+
hasAttachment: !!attachment,
|
|
952
|
+
attachmentBytes:
|
|
953
|
+
attachment && Number.isFinite(attachment.sizeBytes)
|
|
954
|
+
? Math.floor(attachment.sizeBytes)
|
|
955
|
+
: null,
|
|
763
956
|
};
|
|
764
957
|
}
|
|
765
958
|
|
|
@@ -806,6 +999,9 @@ function createRelay(opts) {
|
|
|
806
999
|
) {
|
|
807
1000
|
patch.thinkingLevel = settings.defaultThinking.trim().toLowerCase();
|
|
808
1001
|
}
|
|
1002
|
+
if (settings && settings.defaultFastMode === true) {
|
|
1003
|
+
patch.fastMode = true;
|
|
1004
|
+
}
|
|
809
1005
|
return Object.keys(patch).length > 0 ? patch : null;
|
|
810
1006
|
}
|
|
811
1007
|
|
|
@@ -879,10 +1075,108 @@ function createRelay(opts) {
|
|
|
879
1075
|
getAgentName() {
|
|
880
1076
|
return upstreamRuntime ? upstreamRuntime.getAgentName() : null;
|
|
881
1077
|
},
|
|
1078
|
+
isPinnedFirstUserMessageKey(sessionKey) {
|
|
1079
|
+
const normalizedSessionKey = normalizeEvenAiSessionKeyForLookup(sessionKey);
|
|
1080
|
+
if (!normalizedSessionKey) {
|
|
1081
|
+
return false;
|
|
1082
|
+
}
|
|
1083
|
+
const trackedThrowawayKeys =
|
|
1084
|
+
typeof evenAiSettingsStore.getTrackedThrowawayKeys === "function"
|
|
1085
|
+
? evenAiSettingsStore.getTrackedThrowawayKeys()
|
|
1086
|
+
: [];
|
|
1087
|
+
return dedupeNormalizedSessionKeys(trackedThrowawayKeys).some(
|
|
1088
|
+
(trackedKey) =>
|
|
1089
|
+
trackedKey.toLowerCase() === normalizedSessionKey.toLowerCase(),
|
|
1090
|
+
);
|
|
1091
|
+
},
|
|
882
1092
|
onSessionStateReset: resetActivityStatusAdapter,
|
|
883
1093
|
onPagesChanged: cachePages,
|
|
884
1094
|
onStatusChanged: broadcastStatus,
|
|
1095
|
+
onSessionModelConfig(config) {
|
|
1096
|
+
applyCurrentSessionModelConfigSnapshot(config);
|
|
1097
|
+
},
|
|
1098
|
+
broadcastSessions: () => broadcastSessions(),
|
|
1099
|
+
broadcastEvenAiSessions: () => broadcastEvenAiSessions(),
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
const relayHealth = createRelayHealthMonitor({
|
|
1103
|
+
emitDebug(event, severity, data) {
|
|
1104
|
+
emitDebug(
|
|
1105
|
+
"relay.health",
|
|
1106
|
+
event,
|
|
1107
|
+
severity,
|
|
1108
|
+
{ sessionKey: sessionService.peekSessionKey() || undefined },
|
|
1109
|
+
() => data,
|
|
1110
|
+
{ force: event === "relay_queue_depth" },
|
|
1111
|
+
);
|
|
1112
|
+
},
|
|
885
1113
|
});
|
|
1114
|
+
relayHealth.start();
|
|
1115
|
+
|
|
1116
|
+
const relayOperationRegistry = createRelayOperationRegistry({
|
|
1117
|
+
emitDebug(event, severity, data, context = {}) {
|
|
1118
|
+
emitDebug(
|
|
1119
|
+
"relay.operation",
|
|
1120
|
+
event,
|
|
1121
|
+
severity,
|
|
1122
|
+
{
|
|
1123
|
+
sessionKey: context.sessionKey || sessionService.peekSessionKey() || undefined,
|
|
1124
|
+
runId: context.runId || undefined,
|
|
1125
|
+
},
|
|
1126
|
+
() => data,
|
|
1127
|
+
);
|
|
1128
|
+
},
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
function isActiveSessionModelConfig(config) {
|
|
1132
|
+
return !!(
|
|
1133
|
+
config &&
|
|
1134
|
+
typeof config.sessionKey === "string" &&
|
|
1135
|
+
(
|
|
1136
|
+
typeof sessionService.isCurrentSession === "function"
|
|
1137
|
+
? sessionService.isCurrentSession(config.sessionKey)
|
|
1138
|
+
: config.sessionKey === sessionService.ensureSessionKey()
|
|
1139
|
+
)
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function applyCurrentSessionModelConfigSnapshot(config) {
|
|
1144
|
+
if (!isActiveSessionModelConfig(config)) {
|
|
1145
|
+
return false;
|
|
1146
|
+
}
|
|
1147
|
+
currentSessionModelConfigSnapshot = config;
|
|
1148
|
+
if (
|
|
1149
|
+
upstreamRuntime &&
|
|
1150
|
+
typeof upstreamRuntime.handleCurrentSessionModelConfigChanged === "function"
|
|
1151
|
+
) {
|
|
1152
|
+
upstreamRuntime.handleCurrentSessionModelConfigChanged().catch((err) => {
|
|
1153
|
+
logger.warn(`[relay] Provider usage rebroadcast failed after session config update: ${err.message}`);
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
return true;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function clearCurrentSessionModelConfigSnapshot(trigger) {
|
|
1160
|
+
currentSessionModelConfigSnapshot = null;
|
|
1161
|
+
if (
|
|
1162
|
+
upstreamRuntime &&
|
|
1163
|
+
typeof upstreamRuntime.handleCurrentSessionModelConfigCleared === "function"
|
|
1164
|
+
) {
|
|
1165
|
+
upstreamRuntime.handleCurrentSessionModelConfigCleared().catch((err) => {
|
|
1166
|
+
logger.warn(`[relay] Provider usage clear broadcast failed after ${trigger}: ${err.message}`);
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// TTL fallback for set_session_title activity label. The tool itself
|
|
1172
|
+
// completes in <50ms but its label can linger if no follow-up activity
|
|
1173
|
+
// arrives (e.g. agent streams a response directly after, with no
|
|
1174
|
+
// intervening activity event). After 1s, synthesize a thinking-status
|
|
1175
|
+
// activity with no tool/label so the renderer falls back to the bare
|
|
1176
|
+
// animated spinner. Any real activity arriving in the meantime cancels
|
|
1177
|
+
// the timer.
|
|
1178
|
+
const SESSION_TITLE_STATUS_FALLBACK_MS = 1500;
|
|
1179
|
+
let sessionTitleStatusFallbackTimer = null;
|
|
886
1180
|
|
|
887
1181
|
function broadcastActivity(rawActivity) {
|
|
888
1182
|
const activity = activityStatusAdapter.augmentActivity(rawActivity || {});
|
|
@@ -905,6 +1199,8 @@ function createRelay(opts) {
|
|
|
905
1199
|
intent: (activity && activity.intent) || null,
|
|
906
1200
|
thinkingSummarySource: (activity && activity.thinkingSummarySource) || null,
|
|
907
1201
|
category: (activity && activity.category) || null,
|
|
1202
|
+
isError: typeof activity.isError === "boolean" ? activity.isError : null,
|
|
1203
|
+
code: (activity && activity.code) || null,
|
|
908
1204
|
activityId: (activity && activity.activityId) || null,
|
|
909
1205
|
seq: Number.isFinite(activity && activity.seq) ? activity.seq : null,
|
|
910
1206
|
origin,
|
|
@@ -913,9 +1209,194 @@ function createRelay(opts) {
|
|
|
913
1209
|
);
|
|
914
1210
|
|
|
915
1211
|
server.broadcast(handler.formatActivity(activity));
|
|
1212
|
+
|
|
1213
|
+
if (sessionTitleStatusFallbackTimer) {
|
|
1214
|
+
clearTimeout(sessionTitleStatusFallbackTimer);
|
|
1215
|
+
sessionTitleStatusFallbackTimer = null;
|
|
1216
|
+
}
|
|
1217
|
+
if (
|
|
1218
|
+
activity &&
|
|
1219
|
+
activity.tool === "set_session_title" &&
|
|
1220
|
+
phase !== "end" &&
|
|
1221
|
+
origin !== "synthetic_session_title_fallback"
|
|
1222
|
+
) {
|
|
1223
|
+
const fallbackSessionKey = activity.sessionKey || null;
|
|
1224
|
+
const fallbackRunId = runId;
|
|
1225
|
+
sessionTitleStatusFallbackTimer = setTimeout(() => {
|
|
1226
|
+
sessionTitleStatusFallbackTimer = null;
|
|
1227
|
+
broadcastActivity({
|
|
1228
|
+
state: "thinking",
|
|
1229
|
+
sessionKey: fallbackSessionKey,
|
|
1230
|
+
runId: fallbackRunId,
|
|
1231
|
+
origin: "synthetic_session_title_fallback",
|
|
1232
|
+
phase: "update",
|
|
1233
|
+
});
|
|
1234
|
+
}, SESSION_TITLE_STATUS_FALLBACK_MS);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
916
1237
|
return activity;
|
|
917
1238
|
}
|
|
918
1239
|
|
|
1240
|
+
function broadcastProviderUsageSnapshot(snapshot) {
|
|
1241
|
+
if (!server || !handler || typeof handler.formatProviderUsageSnapshot !== "function") {
|
|
1242
|
+
return snapshot;
|
|
1243
|
+
}
|
|
1244
|
+
server.broadcast(handler.formatProviderUsageSnapshot(snapshot || {}));
|
|
1245
|
+
return snapshot;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
const appClientDisconnectHandlers = new Set();
|
|
1249
|
+
function onAppClientDisconnect(handler) {
|
|
1250
|
+
if (typeof handler !== "function") return () => {};
|
|
1251
|
+
appClientDisconnectHandlers.add(handler);
|
|
1252
|
+
return () => appClientDisconnectHandlers.delete(handler);
|
|
1253
|
+
}
|
|
1254
|
+
function dispatchAppClientDisconnect(sessionKey) {
|
|
1255
|
+
for (const handler of appClientDisconnectHandlers) {
|
|
1256
|
+
try { handler({ sessionKey }); } catch (err) {
|
|
1257
|
+
logger.warn(`[relay] app_client_disconnect handler threw: ${err && err.message ? err.message : err}`);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const glassesUiResultHandlers = new Set();
|
|
1263
|
+
|
|
1264
|
+
function sendGlassesUiRender(params) {
|
|
1265
|
+
if (!server) return;
|
|
1266
|
+
const payload = {
|
|
1267
|
+
type: "glasses_ui_render",
|
|
1268
|
+
sessionKey: params && typeof params.sessionKey === "string" ? params.sessionKey : null,
|
|
1269
|
+
surfaceId: params && typeof params.surfaceId === "string" ? params.surfaceId : "",
|
|
1270
|
+
depth: Number.isFinite(params && params.depth) ? Math.floor(params.depth) : 1,
|
|
1271
|
+
spec: params && params.spec ? params.spec : null,
|
|
1272
|
+
};
|
|
1273
|
+
server.broadcast(JSON.stringify(payload));
|
|
1274
|
+
emitDebug(
|
|
1275
|
+
"glasses.lifecycle",
|
|
1276
|
+
"surface_send",
|
|
1277
|
+
"debug",
|
|
1278
|
+
{ sessionKey: payload.sessionKey || undefined },
|
|
1279
|
+
() => ({ surfaceId: payload.surfaceId, mode: "render", depth: payload.depth, ...summarizeGlassesUiContent(payload.spec) }),
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function sendGlassesUiSurfaceUpdate(params) {
|
|
1284
|
+
if (!server) return;
|
|
1285
|
+
const patch = params && params.patch ? params.patch : null;
|
|
1286
|
+
if (!patch) return;
|
|
1287
|
+
const cleanPatch = {};
|
|
1288
|
+
if (typeof patch.title === "string") cleanPatch.title = patch.title;
|
|
1289
|
+
if (typeof patch.body === "string") cleanPatch.body = patch.body;
|
|
1290
|
+
if (Array.isArray(patch.items)) {
|
|
1291
|
+
// Items may be plain-string labels (list_surface / label-only) OR
|
|
1292
|
+
// {label, body} objects (list_with_details detail-body ticks). Keep both
|
|
1293
|
+
// shapes; drop anything malformed (no string, no string label).
|
|
1294
|
+
cleanPatch.items = patch.items
|
|
1295
|
+
.map((i) => {
|
|
1296
|
+
if (typeof i === "string") return i;
|
|
1297
|
+
if (i && typeof i === "object" && typeof i.label === "string") {
|
|
1298
|
+
const o = { label: i.label };
|
|
1299
|
+
if (typeof i.body === "string") o.body = i.body;
|
|
1300
|
+
return o;
|
|
1301
|
+
}
|
|
1302
|
+
return null;
|
|
1303
|
+
})
|
|
1304
|
+
.filter((i) => i !== null);
|
|
1305
|
+
}
|
|
1306
|
+
const payload = {
|
|
1307
|
+
type: "glasses_ui_surface_update",
|
|
1308
|
+
sessionKey: params && typeof params.sessionKey === "string" ? params.sessionKey : null,
|
|
1309
|
+
surfaceId: params && typeof params.surfaceId === "string" ? params.surfaceId : "",
|
|
1310
|
+
patch: cleanPatch,
|
|
1311
|
+
};
|
|
1312
|
+
server.broadcast(JSON.stringify(payload));
|
|
1313
|
+
emitDebug(
|
|
1314
|
+
"glasses.lifecycle",
|
|
1315
|
+
"surface_send",
|
|
1316
|
+
"debug",
|
|
1317
|
+
{ sessionKey: payload.sessionKey || undefined },
|
|
1318
|
+
() => ({ surfaceId: payload.surfaceId, mode: "update", ...summarizeGlassesUiContent(cleanPatch) }),
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function onGlassesUiResult(handler) {
|
|
1323
|
+
if (typeof handler !== "function") return () => {};
|
|
1324
|
+
glassesUiResultHandlers.add(handler);
|
|
1325
|
+
return () => glassesUiResultHandlers.delete(handler);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function dispatchGlassesUiResult(frame) {
|
|
1329
|
+
if (!frame || typeof frame !== "object") return;
|
|
1330
|
+
for (const handler of glassesUiResultHandlers) {
|
|
1331
|
+
try {
|
|
1332
|
+
handler({
|
|
1333
|
+
surfaceId: typeof frame.surfaceId === "string" ? frame.surfaceId : "",
|
|
1334
|
+
outcome: frame.outcome,
|
|
1335
|
+
});
|
|
1336
|
+
} catch (err) {
|
|
1337
|
+
logger.warn(`[relay] glasses_ui_result handler threw: ${err.message}`);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
const glassesUiNavEventHandlers = new Set();
|
|
1343
|
+
|
|
1344
|
+
function onGlassesUiNavEvent(handler) {
|
|
1345
|
+
if (typeof handler !== "function") return () => {};
|
|
1346
|
+
glassesUiNavEventHandlers.add(handler);
|
|
1347
|
+
return () => glassesUiNavEventHandlers.delete(handler);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
function dispatchGlassesUiNavEvent(frame) {
|
|
1351
|
+
if (!frame || typeof frame !== "object") return;
|
|
1352
|
+
for (const handler of glassesUiNavEventHandlers) {
|
|
1353
|
+
try {
|
|
1354
|
+
handler({
|
|
1355
|
+
surfaceId: typeof frame.surfaceId === "string" ? frame.surfaceId : "",
|
|
1356
|
+
depth: Number.isFinite(frame.depth) ? Math.max(1, Math.floor(frame.depth)) : 1,
|
|
1357
|
+
});
|
|
1358
|
+
} catch (err) {
|
|
1359
|
+
logger.warn(`[relay] glasses_ui_nav_event handler threw: ${err.message}`);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
const deviceInfoResponseHandlers = new Set();
|
|
1365
|
+
|
|
1366
|
+
function sendDeviceInfoRequest(params) {
|
|
1367
|
+
if (!server) return;
|
|
1368
|
+
const payload = {
|
|
1369
|
+
type: "device_info_request",
|
|
1370
|
+
sessionKey: params && typeof params.sessionKey === "string" ? params.sessionKey : null,
|
|
1371
|
+
requestId: params && typeof params.requestId === "string" ? params.requestId : "",
|
|
1372
|
+
};
|
|
1373
|
+
server.broadcast(JSON.stringify(payload));
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function onDeviceInfoResponse(handler) {
|
|
1377
|
+
if (typeof handler !== "function") return () => {};
|
|
1378
|
+
deviceInfoResponseHandlers.add(handler);
|
|
1379
|
+
return () => deviceInfoResponseHandlers.delete(handler);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
function dispatchDeviceInfoResponse(frame) {
|
|
1383
|
+
if (!frame || typeof frame !== "object") return;
|
|
1384
|
+
for (const handler of deviceInfoResponseHandlers) {
|
|
1385
|
+
try {
|
|
1386
|
+
handler({
|
|
1387
|
+
requestId: typeof frame.requestId === "string" ? frame.requestId : "",
|
|
1388
|
+
ok: frame.ok === true,
|
|
1389
|
+
code: typeof frame.code === "string" ? frame.code : undefined,
|
|
1390
|
+
data: frame.data && typeof frame.data === "object" ? frame.data : undefined,
|
|
1391
|
+
});
|
|
1392
|
+
} catch (err) {
|
|
1393
|
+
logger.warn(
|
|
1394
|
+
`[relay] device_info_response handler threw: ${err && err.message ? err.message : err}`,
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
919
1400
|
function normalizeAttachmentErrorCode(err) {
|
|
920
1401
|
if (!err) return "attachment_upstream_rejected";
|
|
921
1402
|
const code = typeof err.code === "string" ? err.code.trim() : "";
|
|
@@ -948,10 +1429,18 @@ function createRelay(opts) {
|
|
|
948
1429
|
const text = params.text;
|
|
949
1430
|
const sessionKey = params.sessionKey;
|
|
950
1431
|
const attachment = params.attachment || null;
|
|
1432
|
+
const clientDisplaySignals = params.clientDisplaySignals || null;
|
|
951
1433
|
const resolvedSessionKey = sessionKey || sessionService.ensureSessionKey();
|
|
952
1434
|
sessionService.recordFirstSentUserMessage(resolvedSessionKey, text);
|
|
1435
|
+
if (clientDisplaySignals && resolvedSessionKey) {
|
|
1436
|
+
sessionService.recordNeuralSessionNamesEnabled(
|
|
1437
|
+
resolvedSessionKey,
|
|
1438
|
+
clientDisplaySignals.neuralSessionNamesEnabled !== false,
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
953
1441
|
const hasAttachment = !!attachment;
|
|
954
1442
|
const sendStartedAt = Date.now();
|
|
1443
|
+
relayOperationRegistry.markStarted(id);
|
|
955
1444
|
sessionService.invalidateSessionsCache();
|
|
956
1445
|
emitDebug(
|
|
957
1446
|
"relay.protocol",
|
|
@@ -976,13 +1465,24 @@ function createRelay(opts) {
|
|
|
976
1465
|
text,
|
|
977
1466
|
resolvedSessionKey,
|
|
978
1467
|
attachment,
|
|
979
|
-
|
|
1468
|
+
{
|
|
1469
|
+
...currentOcuClawSendOptions(clientDisplaySignals),
|
|
1470
|
+
diagnostic: buildOcuClawSendDiagnostic({
|
|
1471
|
+
...params,
|
|
1472
|
+
sessionKey: resolvedSessionKey,
|
|
1473
|
+
}),
|
|
1474
|
+
},
|
|
980
1475
|
);
|
|
981
1476
|
const upstreamDispatchedAt = Date.now();
|
|
982
1477
|
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1478
|
+
const userContent = buildLocalUserMessageContent(text, attachment);
|
|
1479
|
+
conversationState.addMessage("user", userContent);
|
|
1480
|
+
emitDebug(
|
|
1481
|
+
"openclaw.message",
|
|
1482
|
+
"user_message",
|
|
1483
|
+
"info",
|
|
1484
|
+
{ sessionKey: resolvedSessionKey },
|
|
1485
|
+
() => ({ text: typeof text === "string" ? text : "" }),
|
|
986
1486
|
);
|
|
987
1487
|
broadcastPages();
|
|
988
1488
|
const localPublishDoneAt = Date.now();
|
|
@@ -1005,6 +1505,10 @@ function createRelay(opts) {
|
|
|
1005
1505
|
(result) => {
|
|
1006
1506
|
const ackAt = Date.now();
|
|
1007
1507
|
const runId = result && result.runId ? result.runId : null;
|
|
1508
|
+
relayOperationRegistry.markUpstreamAck(id, {
|
|
1509
|
+
runId,
|
|
1510
|
+
status: result && result.status ? result.status : null,
|
|
1511
|
+
});
|
|
1008
1512
|
if (runId && upstreamRuntime) {
|
|
1009
1513
|
upstreamRuntime.trackAcceptedRun({
|
|
1010
1514
|
runId,
|
|
@@ -1030,8 +1534,16 @@ function createRelay(opts) {
|
|
|
1030
1534
|
return result;
|
|
1031
1535
|
},
|
|
1032
1536
|
(err) => {
|
|
1033
|
-
|
|
1034
|
-
err.errorCode
|
|
1537
|
+
const mirroredErrorCode =
|
|
1538
|
+
err && typeof err.errorCode === "string" && err.errorCode.trim()
|
|
1539
|
+
? err.errorCode.trim()
|
|
1540
|
+
: err && typeof err.code === "string" && err.code.trim()
|
|
1541
|
+
? err.code.trim()
|
|
1542
|
+
: attachment
|
|
1543
|
+
? normalizeAttachmentErrorCode(err)
|
|
1544
|
+
: null;
|
|
1545
|
+
if (mirroredErrorCode && err && typeof err === "object") {
|
|
1546
|
+
err.errorCode = mirroredErrorCode;
|
|
1035
1547
|
}
|
|
1036
1548
|
emitDebug(
|
|
1037
1549
|
"relay.protocol",
|
|
@@ -1042,7 +1554,8 @@ function createRelay(opts) {
|
|
|
1042
1554
|
messageId: id,
|
|
1043
1555
|
elapsedMs: Date.now() - sendStartedAt,
|
|
1044
1556
|
hasAttachment,
|
|
1045
|
-
errorCode:
|
|
1557
|
+
errorCode:
|
|
1558
|
+
err && typeof err.errorCode === "string" ? err.errorCode : null,
|
|
1046
1559
|
message: err && err.message ? err.message : String(err),
|
|
1047
1560
|
}),
|
|
1048
1561
|
);
|
|
@@ -1074,13 +1587,39 @@ function createRelay(opts) {
|
|
|
1074
1587
|
};
|
|
1075
1588
|
}
|
|
1076
1589
|
|
|
1590
|
+
function emitListenInterceptBroadcast(params = {}) {
|
|
1591
|
+
if (!server || !handler) {
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
const sessionKey = params && typeof params.sessionKey === "string" ? params.sessionKey : null;
|
|
1595
|
+
server.broadcast(handler.formatEvenAiListenIntercepted(sessionKey));
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1077
1598
|
// --- Downstream handler ---
|
|
1078
1599
|
|
|
1079
|
-
/** @type {ReturnType<typeof
|
|
1600
|
+
/** @type {ReturnType<typeof createRelayWorkerSupervisor>|null} */
|
|
1080
1601
|
let server = null;
|
|
1081
1602
|
let evenAiEndpoint = null;
|
|
1082
1603
|
let evenAiRouter = null;
|
|
1083
1604
|
let evenAiRunWaiter = null;
|
|
1605
|
+
const pendingBufferedEvenAiResponses = new Map();
|
|
1606
|
+
let relayApi = null;
|
|
1607
|
+
|
|
1608
|
+
function applyTraceLogSet(clientId, request) {
|
|
1609
|
+
const enabled = !!(request && request.enabled === true);
|
|
1610
|
+
liveUiTraceLogEnabled = enabled;
|
|
1611
|
+
let persisted = false;
|
|
1612
|
+
if (liveUiTraceFlagPath) {
|
|
1613
|
+
try {
|
|
1614
|
+
fs.writeFileSync(liveUiTraceFlagPath, JSON.stringify({ enabled }) + "\n");
|
|
1615
|
+
persisted = true;
|
|
1616
|
+
} catch (err) {
|
|
1617
|
+
logger.warn(`[relay] liveui trace-log flag persist failed: ${err && err.message ? err.message : err}`);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
emitDebug("relay.protocol", "trace_log_set", "info", { sessionKey: sessionService.ensureSessionKey() }, () => ({ clientId, enabled, persisted }));
|
|
1621
|
+
return { ok: true, enabled, persisted, persistedPath: liveUiTraceFlagPath };
|
|
1622
|
+
}
|
|
1084
1623
|
|
|
1085
1624
|
const handler = createDownstreamHandler({
|
|
1086
1625
|
logger,
|
|
@@ -1099,14 +1638,99 @@ function createRelay(opts) {
|
|
|
1099
1638
|
* @param {object|null} attachment - Optional image attachment payload
|
|
1100
1639
|
* @returns {Promise}
|
|
1101
1640
|
*/
|
|
1102
|
-
onSend(id, text, sessionKey, attachment) {
|
|
1641
|
+
onSend(id, text, sessionKey, attachment, clientDisplaySignals) {
|
|
1103
1642
|
return dispatchOcuClawUserSend({
|
|
1104
1643
|
id,
|
|
1105
1644
|
text,
|
|
1106
1645
|
sessionKey,
|
|
1107
1646
|
attachment,
|
|
1647
|
+
clientDisplaySignals: clientDisplaySignals || null,
|
|
1648
|
+
source: "phone_ui",
|
|
1108
1649
|
});
|
|
1109
1650
|
},
|
|
1651
|
+
onGlassesUiResult(frame) {
|
|
1652
|
+
emitDebug(
|
|
1653
|
+
"glasses.lifecycle",
|
|
1654
|
+
"surface_outcome",
|
|
1655
|
+
"debug",
|
|
1656
|
+
{},
|
|
1657
|
+
() => ({ surfaceId: frame && frame.surfaceId, outcome: frame && frame.outcome }),
|
|
1658
|
+
);
|
|
1659
|
+
dispatchGlassesUiResult(frame);
|
|
1660
|
+
},
|
|
1661
|
+
onGlassesUiNavEvent(frame) {
|
|
1662
|
+
emitDebug(
|
|
1663
|
+
"glasses.lifecycle",
|
|
1664
|
+
"nav_event_recv",
|
|
1665
|
+
"debug",
|
|
1666
|
+
{},
|
|
1667
|
+
() => ({ surfaceId: frame && frame.surfaceId, depth: frame && frame.depth }),
|
|
1668
|
+
);
|
|
1669
|
+
dispatchGlassesUiNavEvent(frame);
|
|
1670
|
+
},
|
|
1671
|
+
onDeviceInfoResponse(frame) {
|
|
1672
|
+
dispatchDeviceInfoResponse(frame);
|
|
1673
|
+
},
|
|
1674
|
+
onGlassesUiRenderInject(params) {
|
|
1675
|
+
sendGlassesUiRender(params);
|
|
1676
|
+
},
|
|
1677
|
+
onSetUserSessionTitle(sessionKey, title) {
|
|
1678
|
+
const result = sessionService.setSessionTitle(sessionKey, title, { userSet: true });
|
|
1679
|
+
if (result && result.ok) {
|
|
1680
|
+
broadcastSessions();
|
|
1681
|
+
}
|
|
1682
|
+
},
|
|
1683
|
+
onSetSessionPinned(sessionKey, pinned, kind) {
|
|
1684
|
+
const result = sessionService.setSessionPinned(kind, sessionKey, pinned);
|
|
1685
|
+
if (result && result.ok) {
|
|
1686
|
+
broadcastSessions();
|
|
1687
|
+
}
|
|
1688
|
+
return result;
|
|
1689
|
+
},
|
|
1690
|
+
onCompactSession({ sessionKey }) {
|
|
1691
|
+
if (!upstreamRuntime || typeof upstreamRuntime.compactActiveSession !== "function") {
|
|
1692
|
+
return Promise.resolve({
|
|
1693
|
+
status: "rejected",
|
|
1694
|
+
error: "upstream runtime not ready",
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
return upstreamRuntime.compactActiveSession(sessionKey);
|
|
1698
|
+
},
|
|
1699
|
+
onDeleteSessions(sessionKeys, kind, switchBeforeDelete) {
|
|
1700
|
+
const action = switchBeforeDelete
|
|
1701
|
+
? sessionService.switchAndDeleteSessions(kind, sessionKeys)
|
|
1702
|
+
: sessionService.deleteSessions(kind, sessionKeys);
|
|
1703
|
+
Promise.resolve(action)
|
|
1704
|
+
.then(() => broadcastSessions())
|
|
1705
|
+
.catch((err) => {
|
|
1706
|
+
logger.error(`[relay] deleteSessions failed: ${err && err.message ? err.message : err}`);
|
|
1707
|
+
});
|
|
1708
|
+
},
|
|
1709
|
+
onSearchTranscripts(clientId, query, kind) {
|
|
1710
|
+
Promise.resolve(sessionService.searchTranscripts(kind, query))
|
|
1711
|
+
.then((result) => {
|
|
1712
|
+
if (!server) return;
|
|
1713
|
+
const payload = {
|
|
1714
|
+
type: "ocuclaw.session.transcripts.search.result",
|
|
1715
|
+
query,
|
|
1716
|
+
kind,
|
|
1717
|
+
snippets: result.snippets,
|
|
1718
|
+
truncated: result.truncated,
|
|
1719
|
+
};
|
|
1720
|
+
server.unicast(clientId, JSON.stringify(payload));
|
|
1721
|
+
})
|
|
1722
|
+
.catch((err) => {
|
|
1723
|
+
logger.error(`[relay] searchTranscripts failed: ${err && err.message ? err.message : err}`);
|
|
1724
|
+
if (server) {
|
|
1725
|
+
const payload = {
|
|
1726
|
+
type: "ocuclaw.session.transcripts.search.result",
|
|
1727
|
+
query, kind, snippets: [], truncated: false,
|
|
1728
|
+
};
|
|
1729
|
+
server.unicast(clientId, JSON.stringify(payload));
|
|
1730
|
+
}
|
|
1731
|
+
});
|
|
1732
|
+
},
|
|
1733
|
+
operationRegistry: relayOperationRegistry,
|
|
1110
1734
|
|
|
1111
1735
|
/**
|
|
1112
1736
|
* Inject a fake assistant message into conversation state.
|
|
@@ -1262,6 +1886,9 @@ function createRelay(opts) {
|
|
|
1262
1886
|
{ sessionKey: sessionService.ensureSessionKey() },
|
|
1263
1887
|
() => ({}),
|
|
1264
1888
|
);
|
|
1889
|
+
if (upstreamRuntime && typeof upstreamRuntime.clearTyping === "function") {
|
|
1890
|
+
upstreamRuntime.clearTyping("new_chat");
|
|
1891
|
+
}
|
|
1265
1892
|
sessionService.invalidateSessionsCache();
|
|
1266
1893
|
resetActivityStatusAdapter();
|
|
1267
1894
|
conversationState.clear();
|
|
@@ -1284,6 +1911,10 @@ function createRelay(opts) {
|
|
|
1284
1911
|
|
|
1285
1912
|
onSwitchSession(sessionKey) {
|
|
1286
1913
|
return sessionService.switchToSession(sessionKey).then((pages) => {
|
|
1914
|
+
clearCurrentSessionModelConfigSnapshot("switch_session");
|
|
1915
|
+
if (upstreamRuntime && typeof upstreamRuntime.clearTyping === "function") {
|
|
1916
|
+
upstreamRuntime.clearTyping("switch_session");
|
|
1917
|
+
}
|
|
1287
1918
|
if (upstreamRuntime && typeof upstreamRuntime.handleSessionChanged === "function") {
|
|
1288
1919
|
upstreamRuntime.handleSessionChanged("switch_session");
|
|
1289
1920
|
}
|
|
@@ -1293,6 +1924,10 @@ function createRelay(opts) {
|
|
|
1293
1924
|
|
|
1294
1925
|
async onNewSession() {
|
|
1295
1926
|
const result = await sessionService.newSession();
|
|
1927
|
+
clearCurrentSessionModelConfigSnapshot("new_session");
|
|
1928
|
+
if (upstreamRuntime && typeof upstreamRuntime.clearTyping === "function") {
|
|
1929
|
+
upstreamRuntime.clearTyping("new_session");
|
|
1930
|
+
}
|
|
1296
1931
|
if (upstreamRuntime && typeof upstreamRuntime.handleSessionChanged === "function") {
|
|
1297
1932
|
upstreamRuntime.handleSessionChanged("new_session");
|
|
1298
1933
|
}
|
|
@@ -1319,12 +1954,26 @@ function createRelay(opts) {
|
|
|
1319
1954
|
: Promise.resolve({ skills: [], fetchedAtMs: Date.now(), stale: true });
|
|
1320
1955
|
},
|
|
1321
1956
|
|
|
1957
|
+
onGetProviderUsageSnapshot() {
|
|
1958
|
+
return upstreamRuntime
|
|
1959
|
+
? upstreamRuntime.getProviderUsageSnapshot()
|
|
1960
|
+
: Promise.resolve({
|
|
1961
|
+
sessionKey: null,
|
|
1962
|
+
provider: null,
|
|
1963
|
+
displayName: null,
|
|
1964
|
+
limitingWindowKey: null,
|
|
1965
|
+
windows: [],
|
|
1966
|
+
fetchedAtMs: Date.now(),
|
|
1967
|
+
stale: true,
|
|
1968
|
+
});
|
|
1969
|
+
},
|
|
1970
|
+
|
|
1322
1971
|
onGetSonioxModels() {
|
|
1323
1972
|
return getSonioxModelsSnapshot();
|
|
1324
1973
|
},
|
|
1325
1974
|
|
|
1326
1975
|
onGetStatus() {
|
|
1327
|
-
return buildStatusObject();
|
|
1976
|
+
return buildStatusObject({ includeDownstreamReadiness: true });
|
|
1328
1977
|
},
|
|
1329
1978
|
|
|
1330
1979
|
onGetSessionModelConfig() {
|
|
@@ -1333,7 +1982,13 @@ function createRelay(opts) {
|
|
|
1333
1982
|
|
|
1334
1983
|
async onSetSessionModelConfig(patch) {
|
|
1335
1984
|
const result = await sessionService.setCurrentSessionModelConfig(patch || {});
|
|
1336
|
-
if (
|
|
1985
|
+
if (
|
|
1986
|
+
result &&
|
|
1987
|
+
result.status === "accepted" &&
|
|
1988
|
+
result.config &&
|
|
1989
|
+
isActiveSessionModelConfig(result.config)
|
|
1990
|
+
) {
|
|
1991
|
+
currentSessionModelConfigSnapshot = result.config;
|
|
1337
1992
|
server.broadcast(handler.formatSessionModelConfig(result.config));
|
|
1338
1993
|
}
|
|
1339
1994
|
return result;
|
|
@@ -1348,50 +2003,7 @@ function createRelay(opts) {
|
|
|
1348
2003
|
},
|
|
1349
2004
|
|
|
1350
2005
|
async onGetEvenAiSessions() {
|
|
1351
|
-
|
|
1352
|
-
evenAiRouter && typeof evenAiRouter.getDedicatedSessionKey === "function"
|
|
1353
|
-
? evenAiRouter.getDedicatedSessionKey()
|
|
1354
|
-
: opts.evenAiDedicatedSessionKey;
|
|
1355
|
-
const dedicatedEvenAiKey = normalizeEvenAiSessionKeyForLookup(dedicatedKey);
|
|
1356
|
-
const trackedThrowawayKeys =
|
|
1357
|
-
typeof evenAiSettingsStore.getTrackedThrowawayKeys === "function"
|
|
1358
|
-
? evenAiSettingsStore.getTrackedThrowawayKeys()
|
|
1359
|
-
: [];
|
|
1360
|
-
const normalizedTrackedThrowawayKeys = dedupeNormalizedSessionKeys(
|
|
1361
|
-
trackedThrowawayKeys,
|
|
1362
|
-
);
|
|
1363
|
-
const resolvedSessions = await sessionService.getSessionsByExactKeys([
|
|
1364
|
-
...normalizedTrackedThrowawayKeys,
|
|
1365
|
-
...(dedicatedEvenAiKey ? [dedicatedEvenAiKey] : []),
|
|
1366
|
-
]);
|
|
1367
|
-
const normalizedDedicatedKey = dedicatedEvenAiKey.toLowerCase();
|
|
1368
|
-
const sessions = [];
|
|
1369
|
-
let dedicatedIncluded = false;
|
|
1370
|
-
for (const session of resolvedSessions) {
|
|
1371
|
-
if (
|
|
1372
|
-
!dedicatedIncluded &&
|
|
1373
|
-
session &&
|
|
1374
|
-
typeof session.key === "string" &&
|
|
1375
|
-
session.key.trim().toLowerCase() === normalizedDedicatedKey
|
|
1376
|
-
) {
|
|
1377
|
-
sessions.push(session);
|
|
1378
|
-
dedicatedIncluded = true;
|
|
1379
|
-
continue;
|
|
1380
|
-
}
|
|
1381
|
-
sessions.push(session);
|
|
1382
|
-
}
|
|
1383
|
-
if (!dedicatedIncluded && dedicatedEvenAiKey) {
|
|
1384
|
-
sessions.unshift({
|
|
1385
|
-
key: dedicatedEvenAiKey,
|
|
1386
|
-
updatedAt: 0,
|
|
1387
|
-
preview: "",
|
|
1388
|
-
firstUserMessage: "",
|
|
1389
|
-
});
|
|
1390
|
-
}
|
|
1391
|
-
return {
|
|
1392
|
-
sessions,
|
|
1393
|
-
dedicatedKey,
|
|
1394
|
-
};
|
|
2006
|
+
return buildEvenAiSessionsSnapshot();
|
|
1395
2007
|
},
|
|
1396
2008
|
|
|
1397
2009
|
async onSetEvenAiSettings(patch) {
|
|
@@ -1422,6 +2034,9 @@ function createRelay(opts) {
|
|
|
1422
2034
|
sessionService.invalidateSessionsCache();
|
|
1423
2035
|
resetActivityStatusAdapter();
|
|
1424
2036
|
conversationState.clear();
|
|
2037
|
+
if (upstreamRuntime && typeof upstreamRuntime.clearTyping === "function") {
|
|
2038
|
+
upstreamRuntime.clearTyping("slash_reset");
|
|
2039
|
+
}
|
|
1425
2040
|
conversationState.setAgentName(
|
|
1426
2041
|
(upstreamRuntime ? upstreamRuntime.getAgentName() : null) || "Agent",
|
|
1427
2042
|
);
|
|
@@ -1437,7 +2052,7 @@ function createRelay(opts) {
|
|
|
1437
2052
|
* @returns {boolean} Whether upstream is connected.
|
|
1438
2053
|
*/
|
|
1439
2054
|
isUpstreamConnected() {
|
|
1440
|
-
return
|
|
2055
|
+
return true;
|
|
1441
2056
|
},
|
|
1442
2057
|
|
|
1443
2058
|
onConsoleLog(level, message) {
|
|
@@ -1459,7 +2074,8 @@ function createRelay(opts) {
|
|
|
1459
2074
|
onEventDebug(clientId, payload) {
|
|
1460
2075
|
if (!payload || typeof payload !== "object") return;
|
|
1461
2076
|
const cat = payload.cat;
|
|
1462
|
-
|
|
2077
|
+
const forceStore = isForcedReadinessProofEvent(payload);
|
|
2078
|
+
if (!forceStore && !debugStore.isEnabled(cat)) return;
|
|
1463
2079
|
emitDebug(
|
|
1464
2080
|
cat,
|
|
1465
2081
|
payload.event,
|
|
@@ -1473,6 +2089,7 @@ function createRelay(opts) {
|
|
|
1473
2089
|
clientId,
|
|
1474
2090
|
...(payload.data || {}),
|
|
1475
2091
|
}),
|
|
2092
|
+
{ force: forceStore },
|
|
1476
2093
|
);
|
|
1477
2094
|
},
|
|
1478
2095
|
|
|
@@ -1507,6 +2124,13 @@ function createRelay(opts) {
|
|
|
1507
2124
|
return result;
|
|
1508
2125
|
},
|
|
1509
2126
|
|
|
2127
|
+
onTraceLogSet(clientId, request) {
|
|
2128
|
+
return applyTraceLogSet(clientId, request);
|
|
2129
|
+
},
|
|
2130
|
+
onTraceLogGet() {
|
|
2131
|
+
return { ok: true, enabled: liveUiTraceLogEnabled, persistedPath: liveUiTraceFlagPath };
|
|
2132
|
+
},
|
|
2133
|
+
|
|
1510
2134
|
onDebugDump(clientId, request) {
|
|
1511
2135
|
const result = debugStore.dump(request);
|
|
1512
2136
|
if (!result.ok) {
|
|
@@ -1637,14 +2261,265 @@ function createRelay(opts) {
|
|
|
1637
2261
|
control,
|
|
1638
2262
|
};
|
|
1639
2263
|
},
|
|
2264
|
+
|
|
2265
|
+
onReadinessProbe(clientId, request) {
|
|
2266
|
+
const now = Date.now();
|
|
2267
|
+
const requestId =
|
|
2268
|
+
(typeof request.requestId === "string" && request.requestId.trim()) ||
|
|
2269
|
+
`readiness-${now}-${Math.random().toString(16).slice(2, 8)}`;
|
|
2270
|
+
const sinceMs = Number.isFinite(Number(request && request.sinceMs))
|
|
2271
|
+
? Math.max(0, Math.floor(Number(request.sinceMs)))
|
|
2272
|
+
: now;
|
|
2273
|
+
const snapshot =
|
|
2274
|
+
server && typeof server.getReadinessSnapshot === "function"
|
|
2275
|
+
? server.getReadinessSnapshot()
|
|
2276
|
+
: {
|
|
2277
|
+
connectedClientCount: 0,
|
|
2278
|
+
fanoutRecipientCount: 0,
|
|
2279
|
+
clients: [],
|
|
2280
|
+
};
|
|
2281
|
+
const targetClientId =
|
|
2282
|
+
snapshot &&
|
|
2283
|
+
snapshot.connectedClientCount === 1 &&
|
|
2284
|
+
snapshot.fanoutRecipientCount === 1 &&
|
|
2285
|
+
Array.isArray(snapshot.clients) &&
|
|
2286
|
+
snapshot.clients.length === 1 &&
|
|
2287
|
+
typeof snapshot.clients[0].clientId === "string"
|
|
2288
|
+
? snapshot.clients[0].clientId
|
|
2289
|
+
: null;
|
|
2290
|
+
|
|
2291
|
+
emitDebug(
|
|
2292
|
+
"relay.protocol",
|
|
2293
|
+
"readiness_probe_requested",
|
|
2294
|
+
"info",
|
|
2295
|
+
{ sessionKey: sessionService.ensureSessionKey() },
|
|
2296
|
+
() => ({
|
|
2297
|
+
clientId,
|
|
2298
|
+
requestId,
|
|
2299
|
+
sinceMs,
|
|
2300
|
+
requestedSessionKey:
|
|
2301
|
+
typeof request.sessionKey === "string" && request.sessionKey.trim()
|
|
2302
|
+
? request.sessionKey.trim()
|
|
2303
|
+
: null,
|
|
2304
|
+
connectedClientCount:
|
|
2305
|
+
snapshot && Number.isFinite(snapshot.connectedClientCount)
|
|
2306
|
+
? snapshot.connectedClientCount
|
|
2307
|
+
: 0,
|
|
2308
|
+
fanoutRecipientCount:
|
|
2309
|
+
snapshot && Number.isFinite(snapshot.fanoutRecipientCount)
|
|
2310
|
+
? snapshot.fanoutRecipientCount
|
|
2311
|
+
: 0,
|
|
2312
|
+
}),
|
|
2313
|
+
);
|
|
2314
|
+
|
|
2315
|
+
if (
|
|
2316
|
+
!snapshot ||
|
|
2317
|
+
snapshot.connectedClientCount <= 0 ||
|
|
2318
|
+
snapshot.fanoutRecipientCount <= 0
|
|
2319
|
+
) {
|
|
2320
|
+
return {
|
|
2321
|
+
ok: false,
|
|
2322
|
+
requestId,
|
|
2323
|
+
reasonCode: "no_downstream_client",
|
|
2324
|
+
message: "No downstream app clients connected",
|
|
2325
|
+
};
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
if (
|
|
2329
|
+
snapshot.connectedClientCount > 1 ||
|
|
2330
|
+
snapshot.fanoutRecipientCount > 1 ||
|
|
2331
|
+
!targetClientId
|
|
2332
|
+
) {
|
|
2333
|
+
return {
|
|
2334
|
+
ok: false,
|
|
2335
|
+
requestId,
|
|
2336
|
+
reasonCode: "multi_recipient_fanout",
|
|
2337
|
+
message: "Multiple downstream app clients connected",
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
emitDebug(
|
|
2342
|
+
"relay.protocol",
|
|
2343
|
+
"readiness_probe_dispatched",
|
|
2344
|
+
"info",
|
|
2345
|
+
{ sessionKey: sessionService.ensureSessionKey() },
|
|
2346
|
+
() => ({
|
|
2347
|
+
clientId,
|
|
2348
|
+
requestId,
|
|
2349
|
+
targetClientId,
|
|
2350
|
+
sinceMs,
|
|
2351
|
+
}),
|
|
2352
|
+
);
|
|
2353
|
+
|
|
2354
|
+
return {
|
|
2355
|
+
ok: true,
|
|
2356
|
+
requestId,
|
|
2357
|
+
targetClientId,
|
|
2358
|
+
probe: {
|
|
2359
|
+
requestId,
|
|
2360
|
+
sinceMs,
|
|
2361
|
+
sessionKey:
|
|
2362
|
+
typeof request.sessionKey === "string" && request.sessionKey.trim()
|
|
2363
|
+
? request.sessionKey.trim()
|
|
2364
|
+
: null,
|
|
2365
|
+
},
|
|
2366
|
+
};
|
|
2367
|
+
},
|
|
2368
|
+
|
|
2369
|
+
onAutomationState(clientId, request) {
|
|
2370
|
+
// Mirrors onReadinessProbe (above): identify the single connected app
|
|
2371
|
+
// client via the readiness snapshot, then return a dispatch envelope
|
|
2372
|
+
// that downstream-handler.handleAutomationState wraps into
|
|
2373
|
+
// `automationStateRequest`. Without this callback wired, the handler
|
|
2374
|
+
// returns null and the request is silently dropped at the relay —
|
|
2375
|
+
// simctl/debugctl times out with no failure response, no trace event,
|
|
2376
|
+
// no outbox drop. The lack of wiring was found 2026-05-28 while
|
|
2377
|
+
// validating the streaming-thinking-emoji-demotion fix on the sim.
|
|
2378
|
+
const now = Date.now();
|
|
2379
|
+
const requestId =
|
|
2380
|
+
(typeof request.requestId === "string" && request.requestId.trim()) ||
|
|
2381
|
+
`automation-${now}-${Math.random().toString(16).slice(2, 8)}`;
|
|
2382
|
+
const requestedSessionKey =
|
|
2383
|
+
typeof request.sessionKey === "string" && request.sessionKey.trim()
|
|
2384
|
+
? request.sessionKey.trim()
|
|
2385
|
+
: null;
|
|
2386
|
+
const snapshot =
|
|
2387
|
+
server && typeof server.getReadinessSnapshot === "function"
|
|
2388
|
+
? server.getReadinessSnapshot()
|
|
2389
|
+
: {
|
|
2390
|
+
connectedClientCount: 0,
|
|
2391
|
+
fanoutRecipientCount: 0,
|
|
2392
|
+
clients: [],
|
|
2393
|
+
};
|
|
2394
|
+
const targetEntry =
|
|
2395
|
+
snapshot &&
|
|
2396
|
+
snapshot.connectedClientCount === 1 &&
|
|
2397
|
+
snapshot.fanoutRecipientCount === 1 &&
|
|
2398
|
+
Array.isArray(snapshot.clients) &&
|
|
2399
|
+
snapshot.clients.length === 1
|
|
2400
|
+
? snapshot.clients[0]
|
|
2401
|
+
: null;
|
|
2402
|
+
const targetClientId =
|
|
2403
|
+
targetEntry && typeof targetEntry.clientId === "string"
|
|
2404
|
+
? targetEntry.clientId
|
|
2405
|
+
: null;
|
|
2406
|
+
// A connected app client that has never published a readiness snapshot
|
|
2407
|
+
// cannot answer an automation state request; forwarding anyway would
|
|
2408
|
+
// park the request in pendingAutomationStateRequests with no reply.
|
|
2409
|
+
// Same predicate as the downstream readiness gate; this wired callback
|
|
2410
|
+
// bypasses the normal dispatch path.
|
|
2411
|
+
const readinessPublished =
|
|
2412
|
+
!!(
|
|
2413
|
+
targetEntry &&
|
|
2414
|
+
targetEntry.readinessSnapshot &&
|
|
2415
|
+
Number.isFinite(targetEntry.readinessSnapshot.emittedAtMs)
|
|
2416
|
+
);
|
|
2417
|
+
|
|
2418
|
+
emitDebug(
|
|
2419
|
+
"relay.protocol",
|
|
2420
|
+
"automation_state_requested",
|
|
2421
|
+
"info",
|
|
2422
|
+
{ sessionKey: sessionService.ensureSessionKey() },
|
|
2423
|
+
() => ({
|
|
2424
|
+
clientId,
|
|
2425
|
+
requestId,
|
|
2426
|
+
requestedSessionKey,
|
|
2427
|
+
connectedClientCount:
|
|
2428
|
+
snapshot && Number.isFinite(snapshot.connectedClientCount)
|
|
2429
|
+
? snapshot.connectedClientCount
|
|
2430
|
+
: 0,
|
|
2431
|
+
fanoutRecipientCount:
|
|
2432
|
+
snapshot && Number.isFinite(snapshot.fanoutRecipientCount)
|
|
2433
|
+
? snapshot.fanoutRecipientCount
|
|
2434
|
+
: 0,
|
|
2435
|
+
}),
|
|
2436
|
+
);
|
|
2437
|
+
|
|
2438
|
+
if (
|
|
2439
|
+
!snapshot ||
|
|
2440
|
+
snapshot.connectedClientCount <= 0 ||
|
|
2441
|
+
snapshot.fanoutRecipientCount <= 0
|
|
2442
|
+
) {
|
|
2443
|
+
return {
|
|
2444
|
+
ok: false,
|
|
2445
|
+
requestId,
|
|
2446
|
+
reasonCode: "no_downstream_client",
|
|
2447
|
+
message: "No downstream app clients connected",
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
if (
|
|
2452
|
+
snapshot.connectedClientCount > 1 ||
|
|
2453
|
+
snapshot.fanoutRecipientCount > 1 ||
|
|
2454
|
+
!targetClientId
|
|
2455
|
+
) {
|
|
2456
|
+
return {
|
|
2457
|
+
ok: false,
|
|
2458
|
+
requestId,
|
|
2459
|
+
reasonCode: "multi_recipient_fanout",
|
|
2460
|
+
message: "Multiple downstream app clients connected",
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
if (!readinessPublished) {
|
|
2465
|
+
return {
|
|
2466
|
+
ok: false,
|
|
2467
|
+
requestId,
|
|
2468
|
+
reasonCode: "snapshot_unavailable",
|
|
2469
|
+
message: "Automation state snapshot is unavailable",
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
emitDebug(
|
|
2474
|
+
"relay.protocol",
|
|
2475
|
+
"automation_state_dispatched",
|
|
2476
|
+
"info",
|
|
2477
|
+
{ sessionKey: sessionService.ensureSessionKey() },
|
|
2478
|
+
() => ({
|
|
2479
|
+
clientId,
|
|
2480
|
+
requestId,
|
|
2481
|
+
targetClientId,
|
|
2482
|
+
}),
|
|
2483
|
+
);
|
|
2484
|
+
|
|
2485
|
+
return {
|
|
2486
|
+
ok: true,
|
|
2487
|
+
requestId,
|
|
2488
|
+
targetClientId,
|
|
2489
|
+
request: {
|
|
2490
|
+
requestId,
|
|
2491
|
+
sessionKey: requestedSessionKey,
|
|
2492
|
+
},
|
|
2493
|
+
};
|
|
2494
|
+
},
|
|
1640
2495
|
});
|
|
1641
2496
|
|
|
1642
|
-
// ---
|
|
2497
|
+
// --- Worker supervisor ---
|
|
1643
2498
|
|
|
1644
|
-
|
|
2499
|
+
const pluginUpdateService = createPluginUpdateService({
|
|
2500
|
+
spawn: childProcess.spawn,
|
|
2501
|
+
logger,
|
|
2502
|
+
nowMs: () => Date.now(),
|
|
2503
|
+
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
2504
|
+
clearTimeout: (handle) => clearTimeout(handle),
|
|
2505
|
+
});
|
|
2506
|
+
|
|
2507
|
+
server = createRelayWorkerSupervisor({
|
|
2508
|
+
pluginId: "ocuclaw",
|
|
2509
|
+
getPluginVersion: () => pluginUpdateService.getPluginVersion(),
|
|
2510
|
+
getRequiresClientVersion: () => pluginUpdateService.getRequiresClientVersion(),
|
|
1645
2511
|
logger,
|
|
1646
|
-
externalDebugToolsEnabled,
|
|
1647
2512
|
handler,
|
|
2513
|
+
operationRegistry: relayOperationRegistry,
|
|
2514
|
+
host: opts.host,
|
|
2515
|
+
port: opts.port,
|
|
2516
|
+
token: opts.token,
|
|
2517
|
+
externalDebugToolsEnabled,
|
|
2518
|
+
runPluginUpdate: () => pluginUpdateService.runPluginUpdate(),
|
|
2519
|
+
runGatewayRestart: () => pluginUpdateService.runGatewayRestart(),
|
|
2520
|
+
evenAiRequestTimeoutMs: opts.evenAiRequestTimeoutMs,
|
|
2521
|
+
evenAiMaxBodyBytes: opts.evenAiMaxBodyBytes,
|
|
2522
|
+
evenAiMaxResponseBytes: opts.evenAiMaxResponseBytes,
|
|
1648
2523
|
getCurrentPages() {
|
|
1649
2524
|
return cachedPages;
|
|
1650
2525
|
},
|
|
@@ -1660,95 +2535,58 @@ function createRelay(opts) {
|
|
|
1660
2535
|
statusRevision: statusRevision || 0,
|
|
1661
2536
|
};
|
|
1662
2537
|
},
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
()
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
remoteAddress: meta && meta.remoteAddress ? meta.remoteAddress : null,
|
|
1674
|
-
userAgentTail: meta && meta.userAgent ? meta.userAgent : null,
|
|
1675
|
-
}),
|
|
1676
|
-
);
|
|
2538
|
+
getAgentAvatarHash: () =>
|
|
2539
|
+
upstreamRuntime && typeof upstreamRuntime.getAgentAvatarHash === "function"
|
|
2540
|
+
? upstreamRuntime.getAgentAvatarHash()
|
|
2541
|
+
: null,
|
|
2542
|
+
getAgentAvatarDataUriByHash: (hash) =>
|
|
2543
|
+
upstreamRuntime && typeof upstreamRuntime.getAgentAvatarDataUriByHash === "function"
|
|
2544
|
+
? upstreamRuntime.getAgentAvatarDataUriByHash(hash)
|
|
2545
|
+
: null,
|
|
2546
|
+
handleBufferedEvenAiHttpRequest(envelope) {
|
|
2547
|
+
return handleBufferedEvenAiHttpRequest(envelope);
|
|
1677
2548
|
},
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
"relay.session",
|
|
1681
|
-
"downstream_client_disconnected",
|
|
1682
|
-
"info",
|
|
1683
|
-
{ sessionKey: sessionService.peekSessionKey() || undefined },
|
|
1684
|
-
() => ({
|
|
1685
|
-
clientId: meta && meta.clientId ? meta.clientId : null,
|
|
1686
|
-
connectedCount: meta && Number.isFinite(meta.connectedCount) ? meta.connectedCount : null,
|
|
1687
|
-
connectedAtMs: meta && Number.isFinite(meta.connectedAtMs) ? meta.connectedAtMs : null,
|
|
1688
|
-
lifetimeMs: meta && Number.isFinite(meta.lifetimeMs) ? meta.lifetimeMs : null,
|
|
1689
|
-
closeCode: meta && Number.isFinite(meta.closeCode) ? meta.closeCode : null,
|
|
1690
|
-
closeReasonTail: meta && meta.closeReason ? meta.closeReason : null,
|
|
1691
|
-
role: meta && meta.role ? meta.role : null,
|
|
1692
|
-
clientKind: meta && meta.clientKind ? meta.clientKind : null,
|
|
1693
|
-
protocolVersion: meta && meta.protocolVersion ? meta.protocolVersion : null,
|
|
1694
|
-
protocolReason: meta && meta.protocolReason ? meta.protocolReason : null,
|
|
1695
|
-
clientName: meta && meta.clientName ? meta.clientName : null,
|
|
1696
|
-
clientVersion: meta && meta.clientVersion ? meta.clientVersion : null,
|
|
1697
|
-
firstMessageType: meta && meta.firstMessageType ? meta.firstMessageType : null,
|
|
1698
|
-
textMessageCount: meta && Number.isFinite(meta.textMessageCount) ? meta.textMessageCount : null,
|
|
1699
|
-
binaryMessageCount: meta && Number.isFinite(meta.binaryMessageCount) ? meta.binaryMessageCount : null,
|
|
1700
|
-
remoteControlCount: meta && Number.isFinite(meta.remoteControlCount) ? meta.remoteControlCount : null,
|
|
1701
|
-
}),
|
|
1702
|
-
);
|
|
2549
|
+
cancelBufferedEvenAiHttpRequest(envelope) {
|
|
2550
|
+
return cancelBufferedEvenAiHttpRequest(envelope);
|
|
1703
2551
|
},
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
{ sessionKey: meta.sessionKey || sessionService.peekSessionKey() || undefined },
|
|
1713
|
-
() => ({
|
|
1714
|
-
clientId: meta && meta.clientId ? meta.clientId : null,
|
|
1715
|
-
state: meta && meta.state ? meta.state : null,
|
|
1716
|
-
connectedCount: meta && Number.isFinite(meta.connectedCount) ? meta.connectedCount : null,
|
|
1717
|
-
role: meta && meta.role ? meta.role : null,
|
|
1718
|
-
clientKind: meta && meta.clientKind ? meta.clientKind : null,
|
|
1719
|
-
clientName: meta && meta.clientName ? meta.clientName : null,
|
|
1720
|
-
clientVersion: meta && meta.clientVersion ? meta.clientVersion : null,
|
|
1721
|
-
protocolVersion: meta && meta.protocolVersion ? meta.protocolVersion : null,
|
|
1722
|
-
}),
|
|
1723
|
-
);
|
|
2552
|
+
getActiveSessionKey() {
|
|
2553
|
+
return sessionService.peekSessionKey() || null;
|
|
2554
|
+
},
|
|
2555
|
+
onAppClientDisconnect(sessionKey) {
|
|
2556
|
+
dispatchAppClientDisconnect(sessionKey);
|
|
2557
|
+
},
|
|
2558
|
+
emitDebug(category, event, severity, context, payloadFactory, options) {
|
|
2559
|
+
emitDebug(category, event, severity, context, payloadFactory, options);
|
|
1724
2560
|
},
|
|
1725
|
-
httpServer: sharedHttpServer,
|
|
1726
|
-
port: opts.port,
|
|
1727
|
-
host: opts.host,
|
|
1728
|
-
token: opts.token,
|
|
1729
2561
|
});
|
|
1730
|
-
if (ownedHttpServer) {
|
|
1731
|
-
ownedHttpServer.on("listening", () => {
|
|
1732
|
-
server.wss.emit("listening");
|
|
1733
|
-
});
|
|
1734
|
-
ownedHttpServer.on("error", (err) => {
|
|
1735
|
-
server.wss.emit("error", err);
|
|
1736
|
-
});
|
|
1737
|
-
listenOwnedRelayHttpServer(ownedHttpServer, opts.host, opts.port);
|
|
1738
|
-
}
|
|
1739
2562
|
|
|
1740
2563
|
// --- Helpers ---
|
|
1741
2564
|
|
|
1742
|
-
function buildStatusObject() {
|
|
1743
|
-
|
|
2565
|
+
function buildStatusObject(options = {}) {
|
|
2566
|
+
const includeDownstreamReadiness = options.includeDownstreamReadiness === true;
|
|
2567
|
+
const status = {
|
|
1744
2568
|
openclaw:
|
|
1745
2569
|
upstreamRuntime && upstreamRuntime.isConnected()
|
|
1746
2570
|
? "connected"
|
|
1747
2571
|
: "disconnected",
|
|
1748
2572
|
agent: upstreamRuntime ? upstreamRuntime.getAgentName() : null,
|
|
2573
|
+
agentEmoji: upstreamRuntime ? upstreamRuntime.getAgentEmoji() : null,
|
|
2574
|
+
agentAvatarHash: upstreamRuntime ? upstreamRuntime.getAgentAvatarHash() : null,
|
|
1749
2575
|
session: sessionService.ensureSessionKey(),
|
|
1750
2576
|
evenAiEnabled: opts.evenAiEnabled === true,
|
|
1751
2577
|
};
|
|
2578
|
+
if (includeDownstreamReadiness) {
|
|
2579
|
+
status.downstreamReadiness =
|
|
2580
|
+
server && typeof server.getReadinessSnapshot === "function"
|
|
2581
|
+
? server.getReadinessSnapshot()
|
|
2582
|
+
: {
|
|
2583
|
+
connectedClientCount: 0,
|
|
2584
|
+
fanoutRecipientCount: 0,
|
|
2585
|
+
updatedAtMs: null,
|
|
2586
|
+
clients: [],
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
return status;
|
|
1752
2590
|
}
|
|
1753
2591
|
|
|
1754
2592
|
function cachePages(pages) {
|
|
@@ -1782,6 +2620,97 @@ function createRelay(opts) {
|
|
|
1782
2620
|
}
|
|
1783
2621
|
}
|
|
1784
2622
|
|
|
2623
|
+
/**
|
|
2624
|
+
* Fetch the latest sessions snapshot and broadcast it. Used after a session
|
|
2625
|
+
* title changes so connected clients refresh the title in the main webui
|
|
2626
|
+
* status row and Session Settings tab without waiting for a manual
|
|
2627
|
+
* session-list open.
|
|
2628
|
+
*/
|
|
2629
|
+
function broadcastSessions() {
|
|
2630
|
+
sessionService
|
|
2631
|
+
.getSessions()
|
|
2632
|
+
.then((sessions) => {
|
|
2633
|
+
server.broadcast(handler.formatSessions(sessions));
|
|
2634
|
+
})
|
|
2635
|
+
.catch((err) => {
|
|
2636
|
+
emitDebug(
|
|
2637
|
+
"relay.session",
|
|
2638
|
+
"session_broadcast_failed",
|
|
2639
|
+
"debug",
|
|
2640
|
+
{ sessionKey: sessionService.peekSessionKey() || undefined },
|
|
2641
|
+
() => ({ message: err && err.message ? err.message : String(err) }),
|
|
2642
|
+
);
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
/**
|
|
2647
|
+
* Resolve the current Even AI sessions snapshot for unicast/broadcast.
|
|
2648
|
+
* Mirrors the shape that `formatEvenAiSessions` expects.
|
|
2649
|
+
*/
|
|
2650
|
+
async function buildEvenAiSessionsSnapshot() {
|
|
2651
|
+
const dedicatedKey =
|
|
2652
|
+
evenAiRouter && typeof evenAiRouter.getDedicatedSessionKey === "function"
|
|
2653
|
+
? evenAiRouter.getDedicatedSessionKey()
|
|
2654
|
+
: opts.evenAiDedicatedSessionKey;
|
|
2655
|
+
const dedicatedEvenAiKey = normalizeEvenAiSessionKeyForLookup(dedicatedKey);
|
|
2656
|
+
const trackedThrowawayKeys =
|
|
2657
|
+
typeof evenAiSettingsStore.getTrackedThrowawayKeys === "function"
|
|
2658
|
+
? evenAiSettingsStore.getTrackedThrowawayKeys()
|
|
2659
|
+
: [];
|
|
2660
|
+
const normalizedTrackedThrowawayKeys = dedupeNormalizedSessionKeys(
|
|
2661
|
+
trackedThrowawayKeys,
|
|
2662
|
+
);
|
|
2663
|
+
const resolvedSessions = await sessionService.getSessionsByExactKeys([
|
|
2664
|
+
...normalizedTrackedThrowawayKeys,
|
|
2665
|
+
...(dedicatedEvenAiKey ? [dedicatedEvenAiKey] : []),
|
|
2666
|
+
]);
|
|
2667
|
+
const normalizedDedicatedKey = dedicatedEvenAiKey.toLowerCase();
|
|
2668
|
+
const sessions = [];
|
|
2669
|
+
let dedicatedIncluded = false;
|
|
2670
|
+
for (const session of resolvedSessions) {
|
|
2671
|
+
if (
|
|
2672
|
+
!dedicatedIncluded &&
|
|
2673
|
+
session &&
|
|
2674
|
+
typeof session.key === "string" &&
|
|
2675
|
+
session.key.trim().toLowerCase() === normalizedDedicatedKey
|
|
2676
|
+
) {
|
|
2677
|
+
sessions.push(session);
|
|
2678
|
+
dedicatedIncluded = true;
|
|
2679
|
+
continue;
|
|
2680
|
+
}
|
|
2681
|
+
sessions.push(session);
|
|
2682
|
+
}
|
|
2683
|
+
if (!dedicatedIncluded && dedicatedEvenAiKey) {
|
|
2684
|
+
sessions.unshift({
|
|
2685
|
+
key: dedicatedEvenAiKey,
|
|
2686
|
+
updatedAt: 0,
|
|
2687
|
+
preview: "",
|
|
2688
|
+
firstUserMessage: "",
|
|
2689
|
+
});
|
|
2690
|
+
}
|
|
2691
|
+
return { sessions, dedicatedKey };
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
function broadcastEvenAiSessions() {
|
|
2695
|
+
if (!server) return;
|
|
2696
|
+
buildEvenAiSessionsSnapshot()
|
|
2697
|
+
.then((payload) => {
|
|
2698
|
+
server.broadcast(handler.formatEvenAiSessions(payload));
|
|
2699
|
+
})
|
|
2700
|
+
.catch((err) => {
|
|
2701
|
+
emitDebug(
|
|
2702
|
+
"relay.session",
|
|
2703
|
+
"session_broadcast_failed",
|
|
2704
|
+
"debug",
|
|
2705
|
+
{ sessionKey: sessionService.peekSessionKey() || undefined },
|
|
2706
|
+
() => ({
|
|
2707
|
+
kind: "evenai",
|
|
2708
|
+
message: err && err.message ? err.message : String(err),
|
|
2709
|
+
}),
|
|
2710
|
+
);
|
|
2711
|
+
});
|
|
2712
|
+
}
|
|
2713
|
+
|
|
1785
2714
|
/**
|
|
1786
2715
|
* Build, cache, and broadcast the current status.
|
|
1787
2716
|
*/
|
|
@@ -1790,10 +2719,24 @@ function createRelay(opts) {
|
|
|
1790
2719
|
if (next !== null) {
|
|
1791
2720
|
server.broadcast(next);
|
|
1792
2721
|
}
|
|
2722
|
+
if (server && typeof server.notifyAgentAvatarChanged === "function") {
|
|
2723
|
+
const hash =
|
|
2724
|
+
upstreamRuntime && typeof upstreamRuntime.getAgentAvatarHash === "function"
|
|
2725
|
+
? upstreamRuntime.getAgentAvatarHash()
|
|
2726
|
+
: null;
|
|
2727
|
+
const dataUri =
|
|
2728
|
+
hash &&
|
|
2729
|
+
upstreamRuntime &&
|
|
2730
|
+
typeof upstreamRuntime.getAgentAvatarDataUriByHash === "function"
|
|
2731
|
+
? upstreamRuntime.getAgentAvatarDataUriByHash(hash)
|
|
2732
|
+
: null;
|
|
2733
|
+
server.notifyAgentAvatarChanged(hash, dataUri);
|
|
2734
|
+
}
|
|
1793
2735
|
}
|
|
1794
2736
|
|
|
1795
2737
|
upstreamRuntime = createUpstreamRuntime({
|
|
1796
2738
|
logger,
|
|
2739
|
+
stateDir: opts.stateDir,
|
|
1797
2740
|
gatewayBridge,
|
|
1798
2741
|
conversationState,
|
|
1799
2742
|
sessionService,
|
|
@@ -1802,6 +2745,11 @@ function createRelay(opts) {
|
|
|
1802
2745
|
broadcastPages,
|
|
1803
2746
|
broadcastStatus,
|
|
1804
2747
|
broadcastActivity,
|
|
2748
|
+
broadcastProviderUsageSnapshot,
|
|
2749
|
+
operationRegistry: relayOperationRegistry,
|
|
2750
|
+
getCurrentSessionModelConfigSnapshot() {
|
|
2751
|
+
return currentSessionModelConfigSnapshot;
|
|
2752
|
+
},
|
|
1805
2753
|
resetActivityStatusAdapter,
|
|
1806
2754
|
modelsCacheTtlMs: opts.modelsCacheTtlMs,
|
|
1807
2755
|
getServer() {
|
|
@@ -1810,8 +2758,39 @@ function createRelay(opts) {
|
|
|
1810
2758
|
getVoiceRuntime() {
|
|
1811
2759
|
return null;
|
|
1812
2760
|
},
|
|
2761
|
+
gatewayUrl: opts.gatewayUrl,
|
|
2762
|
+
gatewayToken: opts.gatewayToken,
|
|
2763
|
+
fetchAgentAvatar: opts.fetchAgentAvatar,
|
|
1813
2764
|
});
|
|
1814
2765
|
|
|
2766
|
+
// Shared routing gate for session-scoped Even AI defaults (thinking seed,
|
|
2767
|
+
// fast-mode patch): never touch active-routed sessions; always seed fresh
|
|
2768
|
+
// background_new sessions; seed persistent background sessions only before
|
|
2769
|
+
// their first turn exists.
|
|
2770
|
+
async function shouldSeedSessionScopedDefaultForRoute(route) {
|
|
2771
|
+
const routingMode =
|
|
2772
|
+
route && typeof route.routingMode === "string"
|
|
2773
|
+
? route.routingMode.trim().toLowerCase()
|
|
2774
|
+
: "active";
|
|
2775
|
+
const sessionKey =
|
|
2776
|
+
route && typeof route.sessionKey === "string" ? route.sessionKey.trim() : "";
|
|
2777
|
+
if (!sessionKey || routingMode === "active") {
|
|
2778
|
+
return false;
|
|
2779
|
+
}
|
|
2780
|
+
if (routingMode === "background_new") {
|
|
2781
|
+
return true;
|
|
2782
|
+
}
|
|
2783
|
+
if (routingMode !== "background") {
|
|
2784
|
+
return false;
|
|
2785
|
+
}
|
|
2786
|
+
try {
|
|
2787
|
+
const existingSessions = await sessionService.getSessionsByExactKeys([sessionKey]);
|
|
2788
|
+
return existingSessions.length === 0;
|
|
2789
|
+
} catch {
|
|
2790
|
+
return false;
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
|
|
1815
2794
|
if (opts.evenAiEnabled === true) {
|
|
1816
2795
|
evenAiRouter = createEvenAiRouter({
|
|
1817
2796
|
sessionService,
|
|
@@ -1829,7 +2808,7 @@ function createRelay(opts) {
|
|
|
1829
2808
|
logger,
|
|
1830
2809
|
httpServer: sharedHttpServer,
|
|
1831
2810
|
enabled: true,
|
|
1832
|
-
externallyRouted:
|
|
2811
|
+
externallyRouted: true,
|
|
1833
2812
|
token: opts.evenAiToken,
|
|
1834
2813
|
getSettingsSnapshot() {
|
|
1835
2814
|
return evenAiSettingsStore.getSnapshot();
|
|
@@ -1850,6 +2829,9 @@ function createRelay(opts) {
|
|
|
1850
2829
|
emitListenInterceptRecovery(params) {
|
|
1851
2830
|
return emitListenInterceptRecovery(params);
|
|
1852
2831
|
},
|
|
2832
|
+
emitListenInterceptBroadcast(params) {
|
|
2833
|
+
return emitListenInterceptBroadcast(params);
|
|
2834
|
+
},
|
|
1853
2835
|
hasConnectedAppClient() {
|
|
1854
2836
|
return server ? server.getConnectedAppCount() > 0 : false;
|
|
1855
2837
|
},
|
|
@@ -1869,31 +2851,29 @@ function createRelay(opts) {
|
|
|
1869
2851
|
},
|
|
1870
2852
|
async shouldSeedThinkingForRoute(params) {
|
|
1871
2853
|
const route = params && params.route ? params.route : params;
|
|
1872
|
-
const routingMode =
|
|
1873
|
-
route && typeof route.routingMode === "string"
|
|
1874
|
-
? route.routingMode.trim().toLowerCase()
|
|
1875
|
-
: "active";
|
|
1876
2854
|
const thinkingLevel =
|
|
1877
2855
|
params && typeof params.thinkingLevel === "string"
|
|
1878
2856
|
? params.thinkingLevel.trim().toLowerCase()
|
|
1879
2857
|
: "";
|
|
1880
|
-
|
|
1881
|
-
route && typeof route.sessionKey === "string" ? route.sessionKey.trim() : "";
|
|
1882
|
-
if (!thinkingLevel || !sessionKey || routingMode === "active") {
|
|
2858
|
+
if (!thinkingLevel) {
|
|
1883
2859
|
return false;
|
|
1884
2860
|
}
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
2861
|
+
return shouldSeedSessionScopedDefaultForRoute(route);
|
|
2862
|
+
},
|
|
2863
|
+
async seedFastModeForRoute(params) {
|
|
2864
|
+
const route = params && params.route ? params.route : params;
|
|
2865
|
+
const settings = evenAiSettingsStore.getSnapshot();
|
|
2866
|
+
if (!settings || settings.defaultFastMode !== true) {
|
|
1889
2867
|
return false;
|
|
1890
2868
|
}
|
|
1891
|
-
|
|
1892
|
-
const existingSessions = await sessionService.getSessionsByExactKeys([sessionKey]);
|
|
1893
|
-
return existingSessions.length === 0;
|
|
1894
|
-
} catch {
|
|
2869
|
+
if (!(await shouldSeedSessionScopedDefaultForRoute(route))) {
|
|
1895
2870
|
return false;
|
|
1896
2871
|
}
|
|
2872
|
+
const result = await sessionService.setSessionModelConfig(
|
|
2873
|
+
route.sessionKey.trim(),
|
|
2874
|
+
{ fastMode: true },
|
|
2875
|
+
);
|
|
2876
|
+
return !!(result && result.status === "accepted");
|
|
1897
2877
|
},
|
|
1898
2878
|
onSessionActivated(route) {
|
|
1899
2879
|
if (!route || !route.sessionChanged) {
|
|
@@ -1910,15 +2890,70 @@ function createRelay(opts) {
|
|
|
1910
2890
|
});
|
|
1911
2891
|
}
|
|
1912
2892
|
|
|
2893
|
+
async function handleBufferedEvenAiHttpRequest(envelope) {
|
|
2894
|
+
if (!evenAiEndpoint || typeof evenAiEndpoint.handleRequest !== "function") {
|
|
2895
|
+
return {
|
|
2896
|
+
statusCode: 404,
|
|
2897
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
2898
|
+
body: Buffer.from("not found"),
|
|
2899
|
+
};
|
|
2900
|
+
}
|
|
2901
|
+
const req = createBufferedHttpRequest(envelope);
|
|
2902
|
+
const res = createBufferedHttpResponse(opts.evenAiMaxResponseBytes || 262_144);
|
|
2903
|
+
const requestId =
|
|
2904
|
+
envelope && typeof envelope.requestId === "string" ? envelope.requestId : null;
|
|
2905
|
+
if (requestId) {
|
|
2906
|
+
pendingBufferedEvenAiResponses.set(requestId, { req, res });
|
|
2907
|
+
}
|
|
2908
|
+
try {
|
|
2909
|
+
await Promise.resolve(evenAiEndpoint.handleRequest(req, res));
|
|
2910
|
+
if (!res.writableEnded) {
|
|
2911
|
+
res.statusCode = 404;
|
|
2912
|
+
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
2913
|
+
res.end("not found");
|
|
2914
|
+
}
|
|
2915
|
+
return res.toResult();
|
|
2916
|
+
} finally {
|
|
2917
|
+
if (requestId) {
|
|
2918
|
+
pendingBufferedEvenAiResponses.delete(requestId);
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
function cancelBufferedEvenAiHttpRequest(envelope) {
|
|
2924
|
+
const requestId =
|
|
2925
|
+
envelope && typeof envelope.requestId === "string" ? envelope.requestId : null;
|
|
2926
|
+
if (!requestId) {
|
|
2927
|
+
return false;
|
|
2928
|
+
}
|
|
2929
|
+
const pending = pendingBufferedEvenAiResponses.get(requestId);
|
|
2930
|
+
if (!pending) {
|
|
2931
|
+
return false;
|
|
2932
|
+
}
|
|
2933
|
+
pending.res.emit("close");
|
|
2934
|
+
pending.req.emit("close");
|
|
2935
|
+
return true;
|
|
2936
|
+
}
|
|
2937
|
+
|
|
1913
2938
|
// --- Public API ---
|
|
1914
2939
|
|
|
1915
|
-
|
|
2940
|
+
relayApi = {
|
|
2941
|
+
/**
|
|
2942
|
+
* Emit a glasses-UI surface-lifecycle event on the permanent
|
|
2943
|
+
* `glasses.lifecycle` debug category (nav reconcile + cron pause/resume/
|
|
2944
|
+
* tick). Recorded only when the category is enabled via debug-set. Wired
|
|
2945
|
+
* through the relay-service facade into the glasses-ui tool handler + cron
|
|
2946
|
+
* engine. See docs/superpowers/findings/2026-05-30-glasses-ui-phase4-hardware.md.
|
|
2947
|
+
*/
|
|
2948
|
+
emitGlassesUiLifecycle(event, severity, data) {
|
|
2949
|
+
emitDebug("glasses.lifecycle", event, severity, {}, () => data || {});
|
|
2950
|
+
},
|
|
1916
2951
|
/**
|
|
1917
2952
|
* Start the upstream OpenClaw connection.
|
|
1918
2953
|
* The downstream server is already listening from construction.
|
|
1919
|
-
|
|
2954
|
+
*/
|
|
1920
2955
|
start() {
|
|
1921
|
-
|
|
2956
|
+
const startGateway = () => Promise.resolve(gatewayBridge.start()).then(() => {
|
|
1922
2957
|
prefetchSonioxModels("relay_start").catch((err) => {
|
|
1923
2958
|
logger.warn(`[relay] Soniox models prefetch failed: ${err.message}`);
|
|
1924
2959
|
});
|
|
@@ -1926,6 +2961,10 @@ function createRelay(opts) {
|
|
|
1926
2961
|
return upstreamRuntime.start();
|
|
1927
2962
|
}
|
|
1928
2963
|
});
|
|
2964
|
+
if (server && typeof server.start === "function") {
|
|
2965
|
+
return Promise.resolve(server.start()).then(startGateway);
|
|
2966
|
+
}
|
|
2967
|
+
return startGateway();
|
|
1929
2968
|
},
|
|
1930
2969
|
|
|
1931
2970
|
/**
|
|
@@ -1944,10 +2983,12 @@ function createRelay(opts) {
|
|
|
1944
2983
|
if (upstreamRuntime) {
|
|
1945
2984
|
upstreamRuntime.stop();
|
|
1946
2985
|
}
|
|
2986
|
+
relayHealth.stop();
|
|
1947
2987
|
gatewayBridge.stop();
|
|
1948
|
-
return Promise.
|
|
1949
|
-
|
|
1950
|
-
|
|
2988
|
+
return Promise.all([
|
|
2989
|
+
sessionService.flushFirstSentUserMessageCache(),
|
|
2990
|
+
Promise.resolve(server.close()),
|
|
2991
|
+
]).then(() => undefined);
|
|
1951
2992
|
},
|
|
1952
2993
|
|
|
1953
2994
|
handleEvenAiHttpRequest(req, res) {
|
|
@@ -1957,11 +2998,34 @@ function createRelay(opts) {
|
|
|
1957
2998
|
return Promise.resolve(evenAiEndpoint.handleRequest(req, res));
|
|
1958
2999
|
},
|
|
1959
3000
|
|
|
3001
|
+
handleBufferedEvenAiHttpRequest,
|
|
3002
|
+
|
|
1960
3003
|
/** The downstream server instance. */
|
|
1961
3004
|
get server() {
|
|
1962
3005
|
return server;
|
|
1963
3006
|
},
|
|
1964
3007
|
|
|
3008
|
+
get workerReadyForTest() {
|
|
3009
|
+
return server && server.readyPromise ? server.readyPromise : Promise.resolve();
|
|
3010
|
+
},
|
|
3011
|
+
|
|
3012
|
+
get debugStoreForTest() {
|
|
3013
|
+
return debugStore;
|
|
3014
|
+
},
|
|
3015
|
+
|
|
3016
|
+
get liveUiTraceLogEnabledForTest() {
|
|
3017
|
+
return liveUiTraceLogEnabled;
|
|
3018
|
+
},
|
|
3019
|
+
__onTraceLogSetForTest(clientId, request) {
|
|
3020
|
+
return applyTraceLogSet(clientId, request);
|
|
3021
|
+
},
|
|
3022
|
+
|
|
3023
|
+
get operationRegistryForTest() {
|
|
3024
|
+
return relayOperationRegistry;
|
|
3025
|
+
},
|
|
3026
|
+
|
|
3027
|
+
relayHealth,
|
|
3028
|
+
|
|
1965
3029
|
get httpServer() {
|
|
1966
3030
|
return sharedHttpServer;
|
|
1967
3031
|
},
|
|
@@ -1969,7 +3033,90 @@ function createRelay(opts) {
|
|
|
1969
3033
|
getEvenAiSettingsSnapshot() {
|
|
1970
3034
|
return evenAiSettingsStore.getSnapshot();
|
|
1971
3035
|
},
|
|
3036
|
+
|
|
3037
|
+
getSessionTitle(sessionKey) {
|
|
3038
|
+
return sessionService.getSessionTitle(sessionKey);
|
|
3039
|
+
},
|
|
3040
|
+
|
|
3041
|
+
hasRecordedUserMessage(sessionKey) {
|
|
3042
|
+
return sessionService.hasRecordedFirstUserMessage(sessionKey);
|
|
3043
|
+
},
|
|
3044
|
+
|
|
3045
|
+
isNeuralSessionNamesEnabled(sessionKey) {
|
|
3046
|
+
return sessionService.isNeuralSessionNamesEnabled(sessionKey);
|
|
3047
|
+
},
|
|
3048
|
+
|
|
3049
|
+
isSessionUserLocked(sessionKey) {
|
|
3050
|
+
return sessionService.isSessionUserLocked(sessionKey);
|
|
3051
|
+
},
|
|
3052
|
+
|
|
3053
|
+
peekSessionKey() {
|
|
3054
|
+
return sessionService.peekSessionKey();
|
|
3055
|
+
},
|
|
3056
|
+
|
|
3057
|
+
/**
|
|
3058
|
+
* Test/shutdown hook: resolves once the async first-user-message cache
|
|
3059
|
+
* write has fully drained (no write in flight, no dirty mark pending) so
|
|
3060
|
+
* the on-disk file reflects the latest in-memory map.
|
|
3061
|
+
*/
|
|
3062
|
+
flushFirstSentUserMessageCache() {
|
|
3063
|
+
return sessionService.flushFirstSentUserMessageCache();
|
|
3064
|
+
},
|
|
3065
|
+
|
|
3066
|
+
recordNeuralSessionNamesEnabled(sessionKey, enabled) {
|
|
3067
|
+
sessionService.recordNeuralSessionNamesEnabled(sessionKey, enabled);
|
|
3068
|
+
},
|
|
3069
|
+
|
|
3070
|
+
setSessionTitle(sessionKey, title, opts) {
|
|
3071
|
+
const result = sessionService.setSessionTitle(sessionKey, title, opts);
|
|
3072
|
+
if (result && result.ok) {
|
|
3073
|
+
broadcastSessions();
|
|
3074
|
+
}
|
|
3075
|
+
return result;
|
|
3076
|
+
},
|
|
3077
|
+
|
|
3078
|
+
/**
|
|
3079
|
+
* Test-only: direct access to dispatchOcuClawUserSend so integration
|
|
3080
|
+
* tests can drive per-turn signal plumbing without a live downstream
|
|
3081
|
+
* WebSocket connection.
|
|
3082
|
+
*/
|
|
3083
|
+
_dispatchOcuClawUserSend(params) {
|
|
3084
|
+
return dispatchOcuClawUserSend(params || {});
|
|
3085
|
+
},
|
|
3086
|
+
|
|
3087
|
+
sendGlassesUiRender(params) {
|
|
3088
|
+
sendGlassesUiRender(params);
|
|
3089
|
+
},
|
|
3090
|
+
|
|
3091
|
+
sendGlassesUiSurfaceUpdate(params) {
|
|
3092
|
+
sendGlassesUiSurfaceUpdate(params);
|
|
3093
|
+
},
|
|
3094
|
+
|
|
3095
|
+
onGlassesUiResult(handler) {
|
|
3096
|
+
return onGlassesUiResult(handler);
|
|
3097
|
+
},
|
|
3098
|
+
|
|
3099
|
+
onGlassesUiNavEvent(handler) {
|
|
3100
|
+
return onGlassesUiNavEvent(handler);
|
|
3101
|
+
},
|
|
3102
|
+
|
|
3103
|
+
sendDeviceInfoRequest(params) {
|
|
3104
|
+
sendDeviceInfoRequest(params);
|
|
3105
|
+
},
|
|
3106
|
+
|
|
3107
|
+
onDeviceInfoResponse(handler) {
|
|
3108
|
+
return onDeviceInfoResponse(handler);
|
|
3109
|
+
},
|
|
3110
|
+
|
|
3111
|
+
hasConnectedAppClient() {
|
|
3112
|
+
return server ? server.getConnectedAppCount() > 0 : false;
|
|
3113
|
+
},
|
|
3114
|
+
|
|
3115
|
+
onAppClientDisconnect(handler) {
|
|
3116
|
+
return onAppClientDisconnect(handler);
|
|
3117
|
+
},
|
|
1972
3118
|
};
|
|
3119
|
+
return relayApi;
|
|
1973
3120
|
}
|
|
1974
3121
|
|
|
1975
3122
|
const createRelayCore = createRelay;
|