ocuclaw 1.2.4 → 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 +18 -5
- 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 +38 -182
- 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 +754 -83
- package/dist/runtime/downstream-server.js +700 -534
- 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 +1209 -204
- 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 +615 -24
- 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 =
|
|
@@ -373,7 +458,9 @@ function createRelay(opts) {
|
|
|
373
458
|
*/
|
|
374
459
|
function emitDebug(cat, event, severity, context, buildData, options) {
|
|
375
460
|
const force = !!(options && options.force === true);
|
|
376
|
-
if (!force && !debugStore.isEnabled(cat))
|
|
461
|
+
if (!force && !debugStore.isEnabled(cat) && !(liveUiTraceLogEnabled && (cat === "glasses.lifecycle" || cat === "openclaw.message"))) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
377
464
|
|
|
378
465
|
let data = {};
|
|
379
466
|
if (typeof buildData === "function") {
|
|
@@ -384,18 +471,47 @@ function createRelay(opts) {
|
|
|
384
471
|
}
|
|
385
472
|
}
|
|
386
473
|
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
event,
|
|
390
|
-
severity,
|
|
391
|
-
data,
|
|
392
|
-
};
|
|
474
|
+
const ts = debugNow();
|
|
475
|
+
const payload = { ts, cat, event, severity, data };
|
|
393
476
|
|
|
394
477
|
if (context && context.sessionKey) payload.sessionKey = context.sessionKey;
|
|
395
478
|
if (context && context.runId) payload.runId = context.runId;
|
|
396
479
|
if (context && context.screen) payload.screen = context.screen;
|
|
397
480
|
|
|
398
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
|
+
}
|
|
399
515
|
}
|
|
400
516
|
|
|
401
517
|
function isForcedReadinessProofEvent(payload) {
|
|
@@ -439,6 +555,11 @@ function createRelay(opts) {
|
|
|
439
555
|
)
|
|
440
556
|
? Math.max(30, Math.floor(opts.sonioxTemporaryKeyExpiresInSeconds))
|
|
441
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;
|
|
442
563
|
/** @type {Array<{id: string, name: string, supportsMaxEndpointDelay: boolean}>|null} */
|
|
443
564
|
let cachedSonioxModels = null;
|
|
444
565
|
let cachedSonioxModelsFetchedAt = 0;
|
|
@@ -667,18 +788,29 @@ function createRelay(opts) {
|
|
|
667
788
|
throw new Error("fetch is not available for Soniox temporary-key minting");
|
|
668
789
|
}
|
|
669
790
|
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
+
}
|
|
682
814
|
|
|
683
815
|
const rawText =
|
|
684
816
|
response && typeof response.text === "function"
|
|
@@ -764,11 +896,63 @@ function createRelay(opts) {
|
|
|
764
896
|
},
|
|
765
897
|
});
|
|
766
898
|
|
|
767
|
-
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);
|
|
768
926
|
return {
|
|
769
|
-
extraSystemPrompt:
|
|
770
|
-
|
|
771
|
-
|
|
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,
|
|
772
956
|
};
|
|
773
957
|
}
|
|
774
958
|
|
|
@@ -815,6 +999,9 @@ function createRelay(opts) {
|
|
|
815
999
|
) {
|
|
816
1000
|
patch.thinkingLevel = settings.defaultThinking.trim().toLowerCase();
|
|
817
1001
|
}
|
|
1002
|
+
if (settings && settings.defaultFastMode === true) {
|
|
1003
|
+
patch.fastMode = true;
|
|
1004
|
+
}
|
|
818
1005
|
return Object.keys(patch).length > 0 ? patch : null;
|
|
819
1006
|
}
|
|
820
1007
|
|
|
@@ -905,7 +1092,91 @@ function createRelay(opts) {
|
|
|
905
1092
|
onSessionStateReset: resetActivityStatusAdapter,
|
|
906
1093
|
onPagesChanged: cachePages,
|
|
907
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
|
+
},
|
|
908
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;
|
|
909
1180
|
|
|
910
1181
|
function broadcastActivity(rawActivity) {
|
|
911
1182
|
const activity = activityStatusAdapter.augmentActivity(rawActivity || {});
|
|
@@ -928,6 +1199,8 @@ function createRelay(opts) {
|
|
|
928
1199
|
intent: (activity && activity.intent) || null,
|
|
929
1200
|
thinkingSummarySource: (activity && activity.thinkingSummarySource) || null,
|
|
930
1201
|
category: (activity && activity.category) || null,
|
|
1202
|
+
isError: typeof activity.isError === "boolean" ? activity.isError : null,
|
|
1203
|
+
code: (activity && activity.code) || null,
|
|
931
1204
|
activityId: (activity && activity.activityId) || null,
|
|
932
1205
|
seq: Number.isFinite(activity && activity.seq) ? activity.seq : null,
|
|
933
1206
|
origin,
|
|
@@ -936,9 +1209,194 @@ function createRelay(opts) {
|
|
|
936
1209
|
);
|
|
937
1210
|
|
|
938
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
|
+
|
|
939
1237
|
return activity;
|
|
940
1238
|
}
|
|
941
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
|
+
|
|
942
1400
|
function normalizeAttachmentErrorCode(err) {
|
|
943
1401
|
if (!err) return "attachment_upstream_rejected";
|
|
944
1402
|
const code = typeof err.code === "string" ? err.code.trim() : "";
|
|
@@ -971,10 +1429,18 @@ function createRelay(opts) {
|
|
|
971
1429
|
const text = params.text;
|
|
972
1430
|
const sessionKey = params.sessionKey;
|
|
973
1431
|
const attachment = params.attachment || null;
|
|
1432
|
+
const clientDisplaySignals = params.clientDisplaySignals || null;
|
|
974
1433
|
const resolvedSessionKey = sessionKey || sessionService.ensureSessionKey();
|
|
975
1434
|
sessionService.recordFirstSentUserMessage(resolvedSessionKey, text);
|
|
1435
|
+
if (clientDisplaySignals && resolvedSessionKey) {
|
|
1436
|
+
sessionService.recordNeuralSessionNamesEnabled(
|
|
1437
|
+
resolvedSessionKey,
|
|
1438
|
+
clientDisplaySignals.neuralSessionNamesEnabled !== false,
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
976
1441
|
const hasAttachment = !!attachment;
|
|
977
1442
|
const sendStartedAt = Date.now();
|
|
1443
|
+
relayOperationRegistry.markStarted(id);
|
|
978
1444
|
sessionService.invalidateSessionsCache();
|
|
979
1445
|
emitDebug(
|
|
980
1446
|
"relay.protocol",
|
|
@@ -999,13 +1465,24 @@ function createRelay(opts) {
|
|
|
999
1465
|
text,
|
|
1000
1466
|
resolvedSessionKey,
|
|
1001
1467
|
attachment,
|
|
1002
|
-
|
|
1468
|
+
{
|
|
1469
|
+
...currentOcuClawSendOptions(clientDisplaySignals),
|
|
1470
|
+
diagnostic: buildOcuClawSendDiagnostic({
|
|
1471
|
+
...params,
|
|
1472
|
+
sessionKey: resolvedSessionKey,
|
|
1473
|
+
}),
|
|
1474
|
+
},
|
|
1003
1475
|
);
|
|
1004
1476
|
const upstreamDispatchedAt = Date.now();
|
|
1005
1477
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
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 : "" }),
|
|
1009
1486
|
);
|
|
1010
1487
|
broadcastPages();
|
|
1011
1488
|
const localPublishDoneAt = Date.now();
|
|
@@ -1028,6 +1505,10 @@ function createRelay(opts) {
|
|
|
1028
1505
|
(result) => {
|
|
1029
1506
|
const ackAt = Date.now();
|
|
1030
1507
|
const runId = result && result.runId ? result.runId : null;
|
|
1508
|
+
relayOperationRegistry.markUpstreamAck(id, {
|
|
1509
|
+
runId,
|
|
1510
|
+
status: result && result.status ? result.status : null,
|
|
1511
|
+
});
|
|
1031
1512
|
if (runId && upstreamRuntime) {
|
|
1032
1513
|
upstreamRuntime.trackAcceptedRun({
|
|
1033
1514
|
runId,
|
|
@@ -1053,8 +1534,16 @@ function createRelay(opts) {
|
|
|
1053
1534
|
return result;
|
|
1054
1535
|
},
|
|
1055
1536
|
(err) => {
|
|
1056
|
-
|
|
1057
|
-
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;
|
|
1058
1547
|
}
|
|
1059
1548
|
emitDebug(
|
|
1060
1549
|
"relay.protocol",
|
|
@@ -1065,7 +1554,8 @@ function createRelay(opts) {
|
|
|
1065
1554
|
messageId: id,
|
|
1066
1555
|
elapsedMs: Date.now() - sendStartedAt,
|
|
1067
1556
|
hasAttachment,
|
|
1068
|
-
errorCode:
|
|
1557
|
+
errorCode:
|
|
1558
|
+
err && typeof err.errorCode === "string" ? err.errorCode : null,
|
|
1069
1559
|
message: err && err.message ? err.message : String(err),
|
|
1070
1560
|
}),
|
|
1071
1561
|
);
|
|
@@ -1097,13 +1587,39 @@ function createRelay(opts) {
|
|
|
1097
1587
|
};
|
|
1098
1588
|
}
|
|
1099
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
|
+
|
|
1100
1598
|
// --- Downstream handler ---
|
|
1101
1599
|
|
|
1102
|
-
/** @type {ReturnType<typeof
|
|
1600
|
+
/** @type {ReturnType<typeof createRelayWorkerSupervisor>|null} */
|
|
1103
1601
|
let server = null;
|
|
1104
1602
|
let evenAiEndpoint = null;
|
|
1105
1603
|
let evenAiRouter = null;
|
|
1106
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
|
+
}
|
|
1107
1623
|
|
|
1108
1624
|
const handler = createDownstreamHandler({
|
|
1109
1625
|
logger,
|
|
@@ -1122,14 +1638,99 @@ function createRelay(opts) {
|
|
|
1122
1638
|
* @param {object|null} attachment - Optional image attachment payload
|
|
1123
1639
|
* @returns {Promise}
|
|
1124
1640
|
*/
|
|
1125
|
-
onSend(id, text, sessionKey, attachment) {
|
|
1641
|
+
onSend(id, text, sessionKey, attachment, clientDisplaySignals) {
|
|
1126
1642
|
return dispatchOcuClawUserSend({
|
|
1127
1643
|
id,
|
|
1128
1644
|
text,
|
|
1129
1645
|
sessionKey,
|
|
1130
1646
|
attachment,
|
|
1647
|
+
clientDisplaySignals: clientDisplaySignals || null,
|
|
1648
|
+
source: "phone_ui",
|
|
1131
1649
|
});
|
|
1132
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,
|
|
1133
1734
|
|
|
1134
1735
|
/**
|
|
1135
1736
|
* Inject a fake assistant message into conversation state.
|
|
@@ -1285,6 +1886,9 @@ function createRelay(opts) {
|
|
|
1285
1886
|
{ sessionKey: sessionService.ensureSessionKey() },
|
|
1286
1887
|
() => ({}),
|
|
1287
1888
|
);
|
|
1889
|
+
if (upstreamRuntime && typeof upstreamRuntime.clearTyping === "function") {
|
|
1890
|
+
upstreamRuntime.clearTyping("new_chat");
|
|
1891
|
+
}
|
|
1288
1892
|
sessionService.invalidateSessionsCache();
|
|
1289
1893
|
resetActivityStatusAdapter();
|
|
1290
1894
|
conversationState.clear();
|
|
@@ -1307,6 +1911,10 @@ function createRelay(opts) {
|
|
|
1307
1911
|
|
|
1308
1912
|
onSwitchSession(sessionKey) {
|
|
1309
1913
|
return sessionService.switchToSession(sessionKey).then((pages) => {
|
|
1914
|
+
clearCurrentSessionModelConfigSnapshot("switch_session");
|
|
1915
|
+
if (upstreamRuntime && typeof upstreamRuntime.clearTyping === "function") {
|
|
1916
|
+
upstreamRuntime.clearTyping("switch_session");
|
|
1917
|
+
}
|
|
1310
1918
|
if (upstreamRuntime && typeof upstreamRuntime.handleSessionChanged === "function") {
|
|
1311
1919
|
upstreamRuntime.handleSessionChanged("switch_session");
|
|
1312
1920
|
}
|
|
@@ -1316,6 +1924,10 @@ function createRelay(opts) {
|
|
|
1316
1924
|
|
|
1317
1925
|
async onNewSession() {
|
|
1318
1926
|
const result = await sessionService.newSession();
|
|
1927
|
+
clearCurrentSessionModelConfigSnapshot("new_session");
|
|
1928
|
+
if (upstreamRuntime && typeof upstreamRuntime.clearTyping === "function") {
|
|
1929
|
+
upstreamRuntime.clearTyping("new_session");
|
|
1930
|
+
}
|
|
1319
1931
|
if (upstreamRuntime && typeof upstreamRuntime.handleSessionChanged === "function") {
|
|
1320
1932
|
upstreamRuntime.handleSessionChanged("new_session");
|
|
1321
1933
|
}
|
|
@@ -1342,6 +1954,20 @@ function createRelay(opts) {
|
|
|
1342
1954
|
: Promise.resolve({ skills: [], fetchedAtMs: Date.now(), stale: true });
|
|
1343
1955
|
},
|
|
1344
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
|
+
|
|
1345
1971
|
onGetSonioxModels() {
|
|
1346
1972
|
return getSonioxModelsSnapshot();
|
|
1347
1973
|
},
|
|
@@ -1356,7 +1982,13 @@ function createRelay(opts) {
|
|
|
1356
1982
|
|
|
1357
1983
|
async onSetSessionModelConfig(patch) {
|
|
1358
1984
|
const result = await sessionService.setCurrentSessionModelConfig(patch || {});
|
|
1359
|
-
if (
|
|
1985
|
+
if (
|
|
1986
|
+
result &&
|
|
1987
|
+
result.status === "accepted" &&
|
|
1988
|
+
result.config &&
|
|
1989
|
+
isActiveSessionModelConfig(result.config)
|
|
1990
|
+
) {
|
|
1991
|
+
currentSessionModelConfigSnapshot = result.config;
|
|
1360
1992
|
server.broadcast(handler.formatSessionModelConfig(result.config));
|
|
1361
1993
|
}
|
|
1362
1994
|
return result;
|
|
@@ -1371,50 +2003,7 @@ function createRelay(opts) {
|
|
|
1371
2003
|
},
|
|
1372
2004
|
|
|
1373
2005
|
async onGetEvenAiSessions() {
|
|
1374
|
-
|
|
1375
|
-
evenAiRouter && typeof evenAiRouter.getDedicatedSessionKey === "function"
|
|
1376
|
-
? evenAiRouter.getDedicatedSessionKey()
|
|
1377
|
-
: opts.evenAiDedicatedSessionKey;
|
|
1378
|
-
const dedicatedEvenAiKey = normalizeEvenAiSessionKeyForLookup(dedicatedKey);
|
|
1379
|
-
const trackedThrowawayKeys =
|
|
1380
|
-
typeof evenAiSettingsStore.getTrackedThrowawayKeys === "function"
|
|
1381
|
-
? evenAiSettingsStore.getTrackedThrowawayKeys()
|
|
1382
|
-
: [];
|
|
1383
|
-
const normalizedTrackedThrowawayKeys = dedupeNormalizedSessionKeys(
|
|
1384
|
-
trackedThrowawayKeys,
|
|
1385
|
-
);
|
|
1386
|
-
const resolvedSessions = await sessionService.getSessionsByExactKeys([
|
|
1387
|
-
...normalizedTrackedThrowawayKeys,
|
|
1388
|
-
...(dedicatedEvenAiKey ? [dedicatedEvenAiKey] : []),
|
|
1389
|
-
]);
|
|
1390
|
-
const normalizedDedicatedKey = dedicatedEvenAiKey.toLowerCase();
|
|
1391
|
-
const sessions = [];
|
|
1392
|
-
let dedicatedIncluded = false;
|
|
1393
|
-
for (const session of resolvedSessions) {
|
|
1394
|
-
if (
|
|
1395
|
-
!dedicatedIncluded &&
|
|
1396
|
-
session &&
|
|
1397
|
-
typeof session.key === "string" &&
|
|
1398
|
-
session.key.trim().toLowerCase() === normalizedDedicatedKey
|
|
1399
|
-
) {
|
|
1400
|
-
sessions.push(session);
|
|
1401
|
-
dedicatedIncluded = true;
|
|
1402
|
-
continue;
|
|
1403
|
-
}
|
|
1404
|
-
sessions.push(session);
|
|
1405
|
-
}
|
|
1406
|
-
if (!dedicatedIncluded && dedicatedEvenAiKey) {
|
|
1407
|
-
sessions.unshift({
|
|
1408
|
-
key: dedicatedEvenAiKey,
|
|
1409
|
-
updatedAt: 0,
|
|
1410
|
-
preview: "",
|
|
1411
|
-
firstUserMessage: "",
|
|
1412
|
-
});
|
|
1413
|
-
}
|
|
1414
|
-
return {
|
|
1415
|
-
sessions,
|
|
1416
|
-
dedicatedKey,
|
|
1417
|
-
};
|
|
2006
|
+
return buildEvenAiSessionsSnapshot();
|
|
1418
2007
|
},
|
|
1419
2008
|
|
|
1420
2009
|
async onSetEvenAiSettings(patch) {
|
|
@@ -1445,6 +2034,9 @@ function createRelay(opts) {
|
|
|
1445
2034
|
sessionService.invalidateSessionsCache();
|
|
1446
2035
|
resetActivityStatusAdapter();
|
|
1447
2036
|
conversationState.clear();
|
|
2037
|
+
if (upstreamRuntime && typeof upstreamRuntime.clearTyping === "function") {
|
|
2038
|
+
upstreamRuntime.clearTyping("slash_reset");
|
|
2039
|
+
}
|
|
1448
2040
|
conversationState.setAgentName(
|
|
1449
2041
|
(upstreamRuntime ? upstreamRuntime.getAgentName() : null) || "Agent",
|
|
1450
2042
|
);
|
|
@@ -1460,7 +2052,7 @@ function createRelay(opts) {
|
|
|
1460
2052
|
* @returns {boolean} Whether upstream is connected.
|
|
1461
2053
|
*/
|
|
1462
2054
|
isUpstreamConnected() {
|
|
1463
|
-
return
|
|
2055
|
+
return true;
|
|
1464
2056
|
},
|
|
1465
2057
|
|
|
1466
2058
|
onConsoleLog(level, message) {
|
|
@@ -1532,6 +2124,13 @@ function createRelay(opts) {
|
|
|
1532
2124
|
return result;
|
|
1533
2125
|
},
|
|
1534
2126
|
|
|
2127
|
+
onTraceLogSet(clientId, request) {
|
|
2128
|
+
return applyTraceLogSet(clientId, request);
|
|
2129
|
+
},
|
|
2130
|
+
onTraceLogGet() {
|
|
2131
|
+
return { ok: true, enabled: liveUiTraceLogEnabled, persistedPath: liveUiTraceFlagPath };
|
|
2132
|
+
},
|
|
2133
|
+
|
|
1535
2134
|
onDebugDump(clientId, request) {
|
|
1536
2135
|
const result = debugStore.dump(request);
|
|
1537
2136
|
if (!result.ok) {
|
|
@@ -1766,14 +2365,161 @@ function createRelay(opts) {
|
|
|
1766
2365
|
},
|
|
1767
2366
|
};
|
|
1768
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
|
+
},
|
|
1769
2495
|
});
|
|
1770
2496
|
|
|
1771
|
-
// ---
|
|
2497
|
+
// --- Worker supervisor ---
|
|
1772
2498
|
|
|
1773
|
-
|
|
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(),
|
|
1774
2511
|
logger,
|
|
1775
|
-
externalDebugToolsEnabled,
|
|
1776
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,
|
|
1777
2523
|
getCurrentPages() {
|
|
1778
2524
|
return cachedPages;
|
|
1779
2525
|
},
|
|
@@ -1789,82 +2535,30 @@ function createRelay(opts) {
|
|
|
1789
2535
|
statusRevision: statusRevision || 0,
|
|
1790
2536
|
};
|
|
1791
2537
|
},
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
()
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
remoteAddress: meta && meta.remoteAddress ? meta.remoteAddress : null,
|
|
1803
|
-
userAgentTail: meta && meta.userAgent ? meta.userAgent : null,
|
|
1804
|
-
}),
|
|
1805
|
-
);
|
|
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);
|
|
1806
2548
|
},
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
"relay.session",
|
|
1810
|
-
"downstream_client_disconnected",
|
|
1811
|
-
"info",
|
|
1812
|
-
{ sessionKey: sessionService.peekSessionKey() || undefined },
|
|
1813
|
-
() => ({
|
|
1814
|
-
clientId: meta && meta.clientId ? meta.clientId : null,
|
|
1815
|
-
connectedCount: meta && Number.isFinite(meta.connectedCount) ? meta.connectedCount : null,
|
|
1816
|
-
connectedAtMs: meta && Number.isFinite(meta.connectedAtMs) ? meta.connectedAtMs : null,
|
|
1817
|
-
lifetimeMs: meta && Number.isFinite(meta.lifetimeMs) ? meta.lifetimeMs : null,
|
|
1818
|
-
closeCode: meta && Number.isFinite(meta.closeCode) ? meta.closeCode : null,
|
|
1819
|
-
closeReasonTail: meta && meta.closeReason ? meta.closeReason : null,
|
|
1820
|
-
role: meta && meta.role ? meta.role : null,
|
|
1821
|
-
clientKind: meta && meta.clientKind ? meta.clientKind : null,
|
|
1822
|
-
protocolVersion: meta && meta.protocolVersion ? meta.protocolVersion : null,
|
|
1823
|
-
protocolReason: meta && meta.protocolReason ? meta.protocolReason : null,
|
|
1824
|
-
clientName: meta && meta.clientName ? meta.clientName : null,
|
|
1825
|
-
clientVersion: meta && meta.clientVersion ? meta.clientVersion : null,
|
|
1826
|
-
firstMessageType: meta && meta.firstMessageType ? meta.firstMessageType : null,
|
|
1827
|
-
textMessageCount: meta && Number.isFinite(meta.textMessageCount) ? meta.textMessageCount : null,
|
|
1828
|
-
binaryMessageCount: meta && Number.isFinite(meta.binaryMessageCount) ? meta.binaryMessageCount : null,
|
|
1829
|
-
remoteControlCount: meta && Number.isFinite(meta.remoteControlCount) ? meta.remoteControlCount : null,
|
|
1830
|
-
}),
|
|
1831
|
-
);
|
|
2549
|
+
cancelBufferedEvenAiHttpRequest(envelope) {
|
|
2550
|
+
return cancelBufferedEvenAiHttpRequest(envelope);
|
|
1832
2551
|
},
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
{ sessionKey: meta.sessionKey || sessionService.peekSessionKey() || undefined },
|
|
1842
|
-
() => ({
|
|
1843
|
-
clientId: meta && meta.clientId ? meta.clientId : null,
|
|
1844
|
-
state: meta && meta.state ? meta.state : null,
|
|
1845
|
-
connectedCount: meta && Number.isFinite(meta.connectedCount) ? meta.connectedCount : null,
|
|
1846
|
-
role: meta && meta.role ? meta.role : null,
|
|
1847
|
-
clientKind: meta && meta.clientKind ? meta.clientKind : null,
|
|
1848
|
-
clientName: meta && meta.clientName ? meta.clientName : null,
|
|
1849
|
-
clientVersion: meta && meta.clientVersion ? meta.clientVersion : null,
|
|
1850
|
-
protocolVersion: meta && meta.protocolVersion ? meta.protocolVersion : null,
|
|
1851
|
-
}),
|
|
1852
|
-
);
|
|
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);
|
|
1853
2560
|
},
|
|
1854
|
-
httpServer: sharedHttpServer,
|
|
1855
|
-
port: opts.port,
|
|
1856
|
-
host: opts.host,
|
|
1857
|
-
token: opts.token,
|
|
1858
2561
|
});
|
|
1859
|
-
if (ownedHttpServer) {
|
|
1860
|
-
ownedHttpServer.on("listening", () => {
|
|
1861
|
-
server.wss.emit("listening");
|
|
1862
|
-
});
|
|
1863
|
-
ownedHttpServer.on("error", (err) => {
|
|
1864
|
-
server.wss.emit("error", err);
|
|
1865
|
-
});
|
|
1866
|
-
listenOwnedRelayHttpServer(ownedHttpServer, opts.host, opts.port);
|
|
1867
|
-
}
|
|
1868
2562
|
|
|
1869
2563
|
// --- Helpers ---
|
|
1870
2564
|
|
|
@@ -1876,6 +2570,8 @@ function createRelay(opts) {
|
|
|
1876
2570
|
? "connected"
|
|
1877
2571
|
: "disconnected",
|
|
1878
2572
|
agent: upstreamRuntime ? upstreamRuntime.getAgentName() : null,
|
|
2573
|
+
agentEmoji: upstreamRuntime ? upstreamRuntime.getAgentEmoji() : null,
|
|
2574
|
+
agentAvatarHash: upstreamRuntime ? upstreamRuntime.getAgentAvatarHash() : null,
|
|
1879
2575
|
session: sessionService.ensureSessionKey(),
|
|
1880
2576
|
evenAiEnabled: opts.evenAiEnabled === true,
|
|
1881
2577
|
};
|
|
@@ -1924,6 +2620,97 @@ function createRelay(opts) {
|
|
|
1924
2620
|
}
|
|
1925
2621
|
}
|
|
1926
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
|
+
|
|
1927
2714
|
/**
|
|
1928
2715
|
* Build, cache, and broadcast the current status.
|
|
1929
2716
|
*/
|
|
@@ -1932,10 +2719,24 @@ function createRelay(opts) {
|
|
|
1932
2719
|
if (next !== null) {
|
|
1933
2720
|
server.broadcast(next);
|
|
1934
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
|
+
}
|
|
1935
2735
|
}
|
|
1936
2736
|
|
|
1937
2737
|
upstreamRuntime = createUpstreamRuntime({
|
|
1938
2738
|
logger,
|
|
2739
|
+
stateDir: opts.stateDir,
|
|
1939
2740
|
gatewayBridge,
|
|
1940
2741
|
conversationState,
|
|
1941
2742
|
sessionService,
|
|
@@ -1944,6 +2745,11 @@ function createRelay(opts) {
|
|
|
1944
2745
|
broadcastPages,
|
|
1945
2746
|
broadcastStatus,
|
|
1946
2747
|
broadcastActivity,
|
|
2748
|
+
broadcastProviderUsageSnapshot,
|
|
2749
|
+
operationRegistry: relayOperationRegistry,
|
|
2750
|
+
getCurrentSessionModelConfigSnapshot() {
|
|
2751
|
+
return currentSessionModelConfigSnapshot;
|
|
2752
|
+
},
|
|
1947
2753
|
resetActivityStatusAdapter,
|
|
1948
2754
|
modelsCacheTtlMs: opts.modelsCacheTtlMs,
|
|
1949
2755
|
getServer() {
|
|
@@ -1952,8 +2758,39 @@ function createRelay(opts) {
|
|
|
1952
2758
|
getVoiceRuntime() {
|
|
1953
2759
|
return null;
|
|
1954
2760
|
},
|
|
2761
|
+
gatewayUrl: opts.gatewayUrl,
|
|
2762
|
+
gatewayToken: opts.gatewayToken,
|
|
2763
|
+
fetchAgentAvatar: opts.fetchAgentAvatar,
|
|
1955
2764
|
});
|
|
1956
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
|
+
|
|
1957
2794
|
if (opts.evenAiEnabled === true) {
|
|
1958
2795
|
evenAiRouter = createEvenAiRouter({
|
|
1959
2796
|
sessionService,
|
|
@@ -1971,7 +2808,7 @@ function createRelay(opts) {
|
|
|
1971
2808
|
logger,
|
|
1972
2809
|
httpServer: sharedHttpServer,
|
|
1973
2810
|
enabled: true,
|
|
1974
|
-
externallyRouted:
|
|
2811
|
+
externallyRouted: true,
|
|
1975
2812
|
token: opts.evenAiToken,
|
|
1976
2813
|
getSettingsSnapshot() {
|
|
1977
2814
|
return evenAiSettingsStore.getSnapshot();
|
|
@@ -1992,6 +2829,9 @@ function createRelay(opts) {
|
|
|
1992
2829
|
emitListenInterceptRecovery(params) {
|
|
1993
2830
|
return emitListenInterceptRecovery(params);
|
|
1994
2831
|
},
|
|
2832
|
+
emitListenInterceptBroadcast(params) {
|
|
2833
|
+
return emitListenInterceptBroadcast(params);
|
|
2834
|
+
},
|
|
1995
2835
|
hasConnectedAppClient() {
|
|
1996
2836
|
return server ? server.getConnectedAppCount() > 0 : false;
|
|
1997
2837
|
},
|
|
@@ -2011,31 +2851,29 @@ function createRelay(opts) {
|
|
|
2011
2851
|
},
|
|
2012
2852
|
async shouldSeedThinkingForRoute(params) {
|
|
2013
2853
|
const route = params && params.route ? params.route : params;
|
|
2014
|
-
const routingMode =
|
|
2015
|
-
route && typeof route.routingMode === "string"
|
|
2016
|
-
? route.routingMode.trim().toLowerCase()
|
|
2017
|
-
: "active";
|
|
2018
2854
|
const thinkingLevel =
|
|
2019
2855
|
params && typeof params.thinkingLevel === "string"
|
|
2020
2856
|
? params.thinkingLevel.trim().toLowerCase()
|
|
2021
2857
|
: "";
|
|
2022
|
-
|
|
2023
|
-
route && typeof route.sessionKey === "string" ? route.sessionKey.trim() : "";
|
|
2024
|
-
if (!thinkingLevel || !sessionKey || routingMode === "active") {
|
|
2858
|
+
if (!thinkingLevel) {
|
|
2025
2859
|
return false;
|
|
2026
2860
|
}
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
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) {
|
|
2031
2867
|
return false;
|
|
2032
2868
|
}
|
|
2033
|
-
|
|
2034
|
-
const existingSessions = await sessionService.getSessionsByExactKeys([sessionKey]);
|
|
2035
|
-
return existingSessions.length === 0;
|
|
2036
|
-
} catch {
|
|
2869
|
+
if (!(await shouldSeedSessionScopedDefaultForRoute(route))) {
|
|
2037
2870
|
return false;
|
|
2038
2871
|
}
|
|
2872
|
+
const result = await sessionService.setSessionModelConfig(
|
|
2873
|
+
route.sessionKey.trim(),
|
|
2874
|
+
{ fastMode: true },
|
|
2875
|
+
);
|
|
2876
|
+
return !!(result && result.status === "accepted");
|
|
2039
2877
|
},
|
|
2040
2878
|
onSessionActivated(route) {
|
|
2041
2879
|
if (!route || !route.sessionChanged) {
|
|
@@ -2052,15 +2890,70 @@ function createRelay(opts) {
|
|
|
2052
2890
|
});
|
|
2053
2891
|
}
|
|
2054
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
|
+
|
|
2055
2938
|
// --- Public API ---
|
|
2056
2939
|
|
|
2057
|
-
|
|
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
|
+
},
|
|
2058
2951
|
/**
|
|
2059
2952
|
* Start the upstream OpenClaw connection.
|
|
2060
2953
|
* The downstream server is already listening from construction.
|
|
2061
|
-
|
|
2954
|
+
*/
|
|
2062
2955
|
start() {
|
|
2063
|
-
|
|
2956
|
+
const startGateway = () => Promise.resolve(gatewayBridge.start()).then(() => {
|
|
2064
2957
|
prefetchSonioxModels("relay_start").catch((err) => {
|
|
2065
2958
|
logger.warn(`[relay] Soniox models prefetch failed: ${err.message}`);
|
|
2066
2959
|
});
|
|
@@ -2068,6 +2961,10 @@ function createRelay(opts) {
|
|
|
2068
2961
|
return upstreamRuntime.start();
|
|
2069
2962
|
}
|
|
2070
2963
|
});
|
|
2964
|
+
if (server && typeof server.start === "function") {
|
|
2965
|
+
return Promise.resolve(server.start()).then(startGateway);
|
|
2966
|
+
}
|
|
2967
|
+
return startGateway();
|
|
2071
2968
|
},
|
|
2072
2969
|
|
|
2073
2970
|
/**
|
|
@@ -2086,10 +2983,12 @@ function createRelay(opts) {
|
|
|
2086
2983
|
if (upstreamRuntime) {
|
|
2087
2984
|
upstreamRuntime.stop();
|
|
2088
2985
|
}
|
|
2986
|
+
relayHealth.stop();
|
|
2089
2987
|
gatewayBridge.stop();
|
|
2090
|
-
return Promise.
|
|
2091
|
-
|
|
2092
|
-
|
|
2988
|
+
return Promise.all([
|
|
2989
|
+
sessionService.flushFirstSentUserMessageCache(),
|
|
2990
|
+
Promise.resolve(server.close()),
|
|
2991
|
+
]).then(() => undefined);
|
|
2093
2992
|
},
|
|
2094
2993
|
|
|
2095
2994
|
handleEvenAiHttpRequest(req, res) {
|
|
@@ -2099,11 +2998,34 @@ function createRelay(opts) {
|
|
|
2099
2998
|
return Promise.resolve(evenAiEndpoint.handleRequest(req, res));
|
|
2100
2999
|
},
|
|
2101
3000
|
|
|
3001
|
+
handleBufferedEvenAiHttpRequest,
|
|
3002
|
+
|
|
2102
3003
|
/** The downstream server instance. */
|
|
2103
3004
|
get server() {
|
|
2104
3005
|
return server;
|
|
2105
3006
|
},
|
|
2106
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
|
+
|
|
2107
3029
|
get httpServer() {
|
|
2108
3030
|
return sharedHttpServer;
|
|
2109
3031
|
},
|
|
@@ -2111,7 +3033,90 @@ function createRelay(opts) {
|
|
|
2111
3033
|
getEvenAiSettingsSnapshot() {
|
|
2112
3034
|
return evenAiSettingsStore.getSnapshot();
|
|
2113
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
|
+
},
|
|
2114
3118
|
};
|
|
3119
|
+
return relayApi;
|
|
2115
3120
|
}
|
|
2116
3121
|
|
|
2117
3122
|
const createRelayCore = createRelay;
|