ocuclaw 1.2.4 → 1.3.1
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 +21 -6
- package/dist/config/runtime-config.js +84 -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 +56 -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/ocuclaw-settings-store.js +74 -31
- package/dist/runtime/plugin-version-service.js +23 -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 +1293 -225
- 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 +281 -0
- package/dist/runtime/relay-worker-queue.js +202 -0
- package/dist/runtime/relay-worker-supervisor.js +1004 -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 +638 -27
- 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 +581 -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 +1111 -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 +14 -5
- package/skills/glasses-ui/SKILL.md +156 -0
- package/dist/runtime/downstream-server.js +0 -1891
|
@@ -1,9 +1,14 @@
|
|
|
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 { EventEmitter } from "node:events";
|
|
4
|
+
import { createPluginVersionService } from "./plugin-version-service.js";
|
|
4
5
|
import * as conversationStateModule from "../domain/conversation-state.js";
|
|
5
6
|
import { createDebugStore } from "../domain/debug-store.js";
|
|
7
|
+
import { summarizeGlassesUiContent } from "../domain/glasses-ui-content-summary.js";
|
|
6
8
|
import { composeReadabilitySystemPrompt } from "../domain/readability-system-prompt.js";
|
|
9
|
+
import { composeNeuralEmojiReactorSystemPrompt } from "../domain/neural-emoji-reactor-system-prompt.js";
|
|
10
|
+
import { composeNeuralPaceModulatorSystemPrompt } from "../domain/neural-pace-modulator-system-prompt.js";
|
|
11
|
+
import { composeGlassesUiNudgeSystemPrompt } from "../domain/glasses-ui-system-prompt.js";
|
|
7
12
|
import { createActivityStatusAdapter } from "../domain/activity-status-adapter.js";
|
|
8
13
|
import { createEvenAiEndpoint } from "../even-ai/even-ai-endpoint.js";
|
|
9
14
|
import { createEvenAiRouter } from "../even-ai/even-ai-router.js";
|
|
@@ -12,15 +17,23 @@ import { createEvenAiSettingsStore } from "../even-ai/even-ai-settings-store.js"
|
|
|
12
17
|
import { createPluginOpenclawClient } from "../gateway/openclaw-client.js";
|
|
13
18
|
import { createPluginRpcGatewayBridge } from "../gateway/gateway-bridge.js";
|
|
14
19
|
import { createDownstreamHandler } from "./downstream-handler.js";
|
|
15
|
-
import { createDownstreamServer } from "./downstream-server.js";
|
|
16
20
|
import { createOcuClawSettingsStore } from "./ocuclaw-settings-store.js";
|
|
17
|
-
import {
|
|
21
|
+
import { createRelayHealthMonitor } from "./relay-health-monitor.js";
|
|
22
|
+
import { createRelayOperationRegistry } from "./relay-operation-registry.js";
|
|
23
|
+
import { createRelayWorkerSupervisor } from "./relay-worker-supervisor.js";
|
|
24
|
+
import {
|
|
25
|
+
createSessionService,
|
|
26
|
+
NEW_SESSION_GREETING_PROMPT,
|
|
27
|
+
} from "./session-service.js";
|
|
18
28
|
import { createUpstreamRuntime } from "./upstream-runtime.js";
|
|
19
29
|
|
|
20
30
|
const SONIOX_TEMP_KEY_URL = "https://api.soniox.com/v1/auth/temporary-api-key";
|
|
21
31
|
const SONIOX_MODELS_URL = "https://api.soniox.com/v1/models";
|
|
22
32
|
const DEFAULT_SONIOX_TEMP_KEY_EXPIRES_IN_SECONDS = 3600;
|
|
23
|
-
|
|
33
|
+
// Maximum time (ms) to wait for a Soniox temp-key mint before aborting the
|
|
34
|
+
// fetch. 8 s is a conservative cold-path ceiling; tests inject a tiny value
|
|
35
|
+
// via opts.sonioxTemporaryKeyMintTimeoutMs to make assertions fast.
|
|
36
|
+
const DEFAULT_SONIOX_TEMP_KEY_MINT_TIMEOUT_MS = 8000;
|
|
24
37
|
const EVEN_AI_NAMESPACE_PREFIX = "ocuclaw:even-ai";
|
|
25
38
|
const EVEN_AI_NAMESPACE_PREFIX_WITH_DELIMITER = "ocuclaw:even-ai:";
|
|
26
39
|
const LISTEN_INTERCEPT_RECOVERY_ERROR = "Voice interrupted; retry";
|
|
@@ -159,6 +172,10 @@ function normalizeSonioxTemporaryKeyErrorCode(err) {
|
|
|
159
172
|
: "";
|
|
160
173
|
const lowered = message.toLowerCase();
|
|
161
174
|
if (!message) return "soniox_temp_key_request_failed";
|
|
175
|
+
// AbortError from the per-fetch timeout AbortController.
|
|
176
|
+
if (err && err.name === "AbortError") {
|
|
177
|
+
return "soniox_temp_key_mint_timeout";
|
|
178
|
+
}
|
|
162
179
|
if (lowered.includes("api key is not configured")) {
|
|
163
180
|
return "soniox_temp_key_not_configured";
|
|
164
181
|
}
|
|
@@ -210,31 +227,71 @@ function normalizeSonioxModelEntryRows(result) {
|
|
|
210
227
|
return models;
|
|
211
228
|
}
|
|
212
229
|
|
|
213
|
-
function
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
230
|
+
function createBufferedHttpRequest(envelope) {
|
|
231
|
+
const req = new EventEmitter();
|
|
232
|
+
req.method = envelope && envelope.method ? envelope.method : "GET";
|
|
233
|
+
req.url = envelope && envelope.url ? envelope.url : "/";
|
|
234
|
+
req.headers = envelope && envelope.headers && typeof envelope.headers === "object"
|
|
235
|
+
? envelope.headers
|
|
236
|
+
: {};
|
|
237
|
+
req.socket = {
|
|
238
|
+
remoteAddress: "127.0.0.1",
|
|
239
|
+
};
|
|
240
|
+
const body = Buffer.from((envelope && envelope.bodyBase64) || "", "base64");
|
|
241
|
+
process.nextTick(() => {
|
|
242
|
+
if (body.length > 0) {
|
|
243
|
+
req.emit("data", body);
|
|
217
244
|
}
|
|
218
|
-
|
|
219
|
-
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
220
|
-
res.end("not found");
|
|
245
|
+
req.emit("end");
|
|
221
246
|
});
|
|
247
|
+
return req;
|
|
222
248
|
}
|
|
223
249
|
|
|
224
|
-
function
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
250
|
+
function createBufferedHttpResponse(maxResponseBytes) {
|
|
251
|
+
const headers = {};
|
|
252
|
+
const chunks = [];
|
|
253
|
+
let totalBytes = 0;
|
|
254
|
+
const limit = Number.isFinite(maxResponseBytes) && maxResponseBytes > 0
|
|
255
|
+
? Math.floor(maxResponseBytes)
|
|
256
|
+
: 262_144;
|
|
257
|
+
// EventEmitter shape: handlers like the Even-AI endpoint subscribe to
|
|
258
|
+
// res.once('close', ...) for client-disconnect detection. Worker-mode
|
|
259
|
+
// relays actual client closes through an http.cancel worker message.
|
|
260
|
+
const res = new EventEmitter();
|
|
261
|
+
res.statusCode = 200;
|
|
262
|
+
res.writableEnded = false;
|
|
263
|
+
res.setHeader = function (name, value) {
|
|
264
|
+
if (typeof name === "string" && name) {
|
|
265
|
+
headers[name.toLowerCase()] = value;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
res.getHeader = function (name) {
|
|
269
|
+
return typeof name === "string" ? headers[name.toLowerCase()] : undefined;
|
|
270
|
+
};
|
|
271
|
+
res.write = function (chunk) {
|
|
272
|
+
if (this.writableEnded) return false;
|
|
273
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk ?? ""));
|
|
274
|
+
totalBytes += buffer.length;
|
|
275
|
+
if (totalBytes > limit) {
|
|
276
|
+
throw new Error("Buffered HTTP response exceeded relay worker limit");
|
|
277
|
+
}
|
|
278
|
+
chunks.push(buffer);
|
|
279
|
+
return true;
|
|
280
|
+
};
|
|
281
|
+
res.end = function (chunk) {
|
|
282
|
+
if (chunk !== undefined && chunk !== null) {
|
|
283
|
+
this.write(chunk);
|
|
284
|
+
}
|
|
285
|
+
this.writableEnded = true;
|
|
286
|
+
};
|
|
287
|
+
res.toResult = function () {
|
|
288
|
+
return {
|
|
289
|
+
statusCode: this.statusCode,
|
|
290
|
+
headers: { ...headers },
|
|
291
|
+
body: Buffer.concat(chunks),
|
|
292
|
+
};
|
|
293
|
+
};
|
|
294
|
+
return res;
|
|
238
295
|
}
|
|
239
296
|
|
|
240
297
|
// --- Factory ---
|
|
@@ -283,9 +340,7 @@ function createRelay(opts) {
|
|
|
283
340
|
const activityStatusAdapter = createActivityStatusAdapter(
|
|
284
341
|
opts.activityStatusAdapter,
|
|
285
342
|
);
|
|
286
|
-
const
|
|
287
|
-
!opts.httpServer && opts.evenAiEnabled === true ? createOwnedRelayHttpServer() : null;
|
|
288
|
-
const sharedHttpServer = opts.httpServer || ownedHttpServer || null;
|
|
343
|
+
const sharedHttpServer = opts.httpServer || null;
|
|
289
344
|
|
|
290
345
|
// --- Cached state ---
|
|
291
346
|
|
|
@@ -298,6 +353,8 @@ function createRelay(opts) {
|
|
|
298
353
|
let cachedStatus = null;
|
|
299
354
|
/** Monotonic status snapshot revision used for resume handshake. */
|
|
300
355
|
let statusRevision = 0;
|
|
356
|
+
/** @type {{sessionKey: string, modelProvider: string|null, model: string|null, thinkingLevel: string, reasoningLevel: string, verboseLevel: string}|null} */
|
|
357
|
+
let currentSessionModelConfigSnapshot = null;
|
|
301
358
|
|
|
302
359
|
/** Relay-local deterministic simulate-stream run sequence counter. */
|
|
303
360
|
let simulateStreamRunSeq = 0;
|
|
@@ -306,18 +363,74 @@ function createRelay(opts) {
|
|
|
306
363
|
|
|
307
364
|
// --- Structured debug state ---
|
|
308
365
|
|
|
366
|
+
const debugCategories = Array.isArray(opts.debugCategories)
|
|
367
|
+
? opts.debugCategories
|
|
368
|
+
: opts.debugCategories && typeof opts.debugCategories === "object"
|
|
369
|
+
? Object.entries(opts.debugCategories)
|
|
370
|
+
.filter(([, enabled]) => enabled)
|
|
371
|
+
.map(([category]) => category)
|
|
372
|
+
: opts.debugCategories;
|
|
373
|
+
// Single relay-side clock: the debug store, the emitDebug ts stamp, and the
|
|
374
|
+
// liveui log tee all read from this one source so store records and `[liveui]`
|
|
375
|
+
// log lines share an identical ts (downstream reconcilers dedupe on it).
|
|
376
|
+
const debugNow =
|
|
377
|
+
typeof opts.debugNow === "function" ? opts.debugNow : () => Date.now();
|
|
378
|
+
|
|
379
|
+
// --- Durable debug-store arm (survives relay/gateway restarts) ---
|
|
380
|
+
// The capture arm (enabled categories + TTLs) lives only in the in-memory
|
|
381
|
+
// debug-store, which a restart rebuilds empty. We persist it to debug-arm.json
|
|
382
|
+
// (via persistDebugArm) and rehydrate it here at construction — read-once,
|
|
383
|
+
// mirroring liveUiTraceFlagPath below — so a restart no longer silently drops
|
|
384
|
+
// capture. This path covers process RESTART only: a pure WebUI *reload* does NOT
|
|
385
|
+
// restart the relay and already preserves + re-advertises the arm via
|
|
386
|
+
// relay-worker-transport.ts:327-328 (cache.debugConfig re-broadcast to app
|
|
387
|
+
// clients) — do not add reconnect machinery here.
|
|
388
|
+
const debugArmStatePath =
|
|
389
|
+
typeof opts.stateDir === "string" && opts.stateDir
|
|
390
|
+
? path.join(opts.stateDir, "debug-arm.json")
|
|
391
|
+
: null;
|
|
392
|
+
let initialDebugArm = [];
|
|
393
|
+
if (debugArmStatePath) {
|
|
394
|
+
try {
|
|
395
|
+
const parsed = JSON.parse(fs.readFileSync(debugArmStatePath, "utf8"));
|
|
396
|
+
if (parsed && Array.isArray(parsed.enabled)) {
|
|
397
|
+
initialDebugArm = parsed.enabled;
|
|
398
|
+
}
|
|
399
|
+
} catch {
|
|
400
|
+
initialDebugArm = [];
|
|
401
|
+
}
|
|
402
|
+
}
|
|
309
403
|
const debugStore = createDebugStore({
|
|
310
|
-
categories:
|
|
404
|
+
categories: debugCategories,
|
|
311
405
|
capacity: opts.debugCapacity,
|
|
312
406
|
payloadMaxBytes: opts.debugPayloadMaxBytes,
|
|
313
407
|
defaultTtlMs: opts.debugDefaultTtlMs,
|
|
314
408
|
maxTtlMs: opts.debugMaxTtlMs,
|
|
315
409
|
dumpDefaultLimit: opts.debugDumpDefaultLimit,
|
|
316
410
|
dumpMaxLimit: opts.debugDumpMaxLimit,
|
|
317
|
-
now:
|
|
411
|
+
now: debugNow,
|
|
318
412
|
noisyPolicies: opts.debugNoisyPolicies,
|
|
413
|
+
initialEnabled: initialDebugArm,
|
|
319
414
|
});
|
|
320
415
|
|
|
416
|
+
// --- Live-interface trace-log flag (durable across restarts) ---
|
|
417
|
+
// Gates the glasses.lifecycle → gateway-log tee. Read once at construction;
|
|
418
|
+
// toggled live by applyTraceLogSet, which also rewrites this file so the
|
|
419
|
+
// value survives a relay/gateway restart (the store's enable-state does not).
|
|
420
|
+
const liveUiTraceFlagPath =
|
|
421
|
+
typeof opts.stateDir === "string" && opts.stateDir
|
|
422
|
+
? path.join(opts.stateDir, "liveui-trace.json")
|
|
423
|
+
: null;
|
|
424
|
+
let liveUiTraceLogEnabled = false;
|
|
425
|
+
if (liveUiTraceFlagPath) {
|
|
426
|
+
try {
|
|
427
|
+
liveUiTraceLogEnabled =
|
|
428
|
+
JSON.parse(fs.readFileSync(liveUiTraceFlagPath, "utf8")).enabled === true;
|
|
429
|
+
} catch {
|
|
430
|
+
liveUiTraceLogEnabled = false;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
321
434
|
// --- Console log file ---
|
|
322
435
|
|
|
323
436
|
const consoleLogPath =
|
|
@@ -373,7 +486,9 @@ function createRelay(opts) {
|
|
|
373
486
|
*/
|
|
374
487
|
function emitDebug(cat, event, severity, context, buildData, options) {
|
|
375
488
|
const force = !!(options && options.force === true);
|
|
376
|
-
if (!force && !debugStore.isEnabled(cat))
|
|
489
|
+
if (!force && !debugStore.isEnabled(cat) && !(liveUiTraceLogEnabled && (cat === "glasses.lifecycle" || cat === "openclaw.message"))) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
377
492
|
|
|
378
493
|
let data = {};
|
|
379
494
|
if (typeof buildData === "function") {
|
|
@@ -384,18 +499,47 @@ function createRelay(opts) {
|
|
|
384
499
|
}
|
|
385
500
|
}
|
|
386
501
|
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
event,
|
|
390
|
-
severity,
|
|
391
|
-
data,
|
|
392
|
-
};
|
|
502
|
+
const ts = debugNow();
|
|
503
|
+
const payload = { ts, cat, event, severity, data };
|
|
393
504
|
|
|
394
505
|
if (context && context.sessionKey) payload.sessionKey = context.sessionKey;
|
|
395
506
|
if (context && context.runId) payload.runId = context.runId;
|
|
396
507
|
if (context && context.screen) payload.screen = context.screen;
|
|
397
508
|
|
|
398
509
|
debugStore.emit(payload, { force });
|
|
510
|
+
|
|
511
|
+
// Durable openclaw-side trace tee (gated by the persistent flag, NOT the
|
|
512
|
+
// store category enable). Must never throw into the emit path.
|
|
513
|
+
if (liveUiTraceLogEnabled && (cat === "glasses.lifecycle" || cat === "openclaw.message")) {
|
|
514
|
+
try {
|
|
515
|
+
const surfaceId =
|
|
516
|
+
data && typeof data.surfaceId === "string" ? data.surfaceId : null;
|
|
517
|
+
const sessionKey =
|
|
518
|
+
payload.sessionKey ||
|
|
519
|
+
(data && typeof data.sessionKey === "string" ? data.sessionKey : null) ||
|
|
520
|
+
null;
|
|
521
|
+
const side =
|
|
522
|
+
cat === "openclaw.message"
|
|
523
|
+
? (event === "user_message" ? "user" : "agent")
|
|
524
|
+
: "openclaw";
|
|
525
|
+
logger.info(
|
|
526
|
+
"[liveui] " +
|
|
527
|
+
JSON.stringify({
|
|
528
|
+
trace: "liveui",
|
|
529
|
+
side,
|
|
530
|
+
ts,
|
|
531
|
+
cat,
|
|
532
|
+
event,
|
|
533
|
+
severity,
|
|
534
|
+
surfaceId,
|
|
535
|
+
sessionKey,
|
|
536
|
+
data,
|
|
537
|
+
}),
|
|
538
|
+
);
|
|
539
|
+
} catch {
|
|
540
|
+
// observability must never break the emit path
|
|
541
|
+
}
|
|
542
|
+
}
|
|
399
543
|
}
|
|
400
544
|
|
|
401
545
|
function isForcedReadinessProofEvent(payload) {
|
|
@@ -439,6 +583,11 @@ function createRelay(opts) {
|
|
|
439
583
|
)
|
|
440
584
|
? Math.max(30, Math.floor(opts.sonioxTemporaryKeyExpiresInSeconds))
|
|
441
585
|
: DEFAULT_SONIOX_TEMP_KEY_EXPIRES_IN_SECONDS;
|
|
586
|
+
const sonioxTemporaryKeyMintTimeoutMs = Number.isFinite(
|
|
587
|
+
opts.sonioxTemporaryKeyMintTimeoutMs,
|
|
588
|
+
)
|
|
589
|
+
? Math.max(1, Math.floor(opts.sonioxTemporaryKeyMintTimeoutMs))
|
|
590
|
+
: DEFAULT_SONIOX_TEMP_KEY_MINT_TIMEOUT_MS;
|
|
442
591
|
/** @type {Array<{id: string, name: string, supportsMaxEndpointDelay: boolean}>|null} */
|
|
443
592
|
let cachedSonioxModels = null;
|
|
444
593
|
let cachedSonioxModelsFetchedAt = 0;
|
|
@@ -667,18 +816,29 @@ function createRelay(opts) {
|
|
|
667
816
|
throw new Error("fetch is not available for Soniox temporary-key minting");
|
|
668
817
|
}
|
|
669
818
|
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
819
|
+
const mintAbortController = new AbortController();
|
|
820
|
+
const mintTimeoutTimer = setTimeout(
|
|
821
|
+
() => mintAbortController.abort(),
|
|
822
|
+
sonioxTemporaryKeyMintTimeoutMs,
|
|
823
|
+
);
|
|
824
|
+
let response;
|
|
825
|
+
try {
|
|
826
|
+
response = await fetchImpl(SONIOX_TEMP_KEY_URL, {
|
|
827
|
+
method: "POST",
|
|
828
|
+
headers: {
|
|
829
|
+
Authorization: `Bearer ${configuredSonioxApiKey}`,
|
|
830
|
+
"Content-Type": "application/json",
|
|
831
|
+
},
|
|
832
|
+
body: JSON.stringify({
|
|
833
|
+
usage_type: "transcribe_websocket",
|
|
834
|
+
expires_in_seconds: sonioxTemporaryKeyExpiresInSeconds,
|
|
835
|
+
client_reference_id: voiceSessionId,
|
|
836
|
+
}),
|
|
837
|
+
signal: mintAbortController.signal,
|
|
838
|
+
});
|
|
839
|
+
} finally {
|
|
840
|
+
clearTimeout(mintTimeoutTimer);
|
|
841
|
+
}
|
|
682
842
|
|
|
683
843
|
const rawText =
|
|
684
844
|
response && typeof response.text === "function"
|
|
@@ -764,11 +924,63 @@ function createRelay(opts) {
|
|
|
764
924
|
},
|
|
765
925
|
});
|
|
766
926
|
|
|
767
|
-
function currentOcuClawSendOptions() {
|
|
927
|
+
function currentOcuClawSendOptions(perTurnSignals) {
|
|
928
|
+
const signals = perTurnSignals || {};
|
|
929
|
+
const baseReadability = composeReadabilitySystemPrompt(
|
|
930
|
+
ocuClawSettingsStore.getSnapshot().systemPrompt,
|
|
931
|
+
);
|
|
932
|
+
const validState = (raw) =>
|
|
933
|
+
raw === "active" || raw === "recently-disabled" || raw === "inactive"
|
|
934
|
+
? raw
|
|
935
|
+
: "inactive";
|
|
936
|
+
const reactorState = validState(signals.neuralEmojiReactorState);
|
|
937
|
+
const paceState = validState(signals.neuralPaceModulatorState);
|
|
938
|
+
const reactor = composeNeuralEmojiReactorSystemPrompt({ state: reactorState });
|
|
939
|
+
const pace = composeNeuralPaceModulatorSystemPrompt({ state: paceState });
|
|
940
|
+
// Only include the glasses-UI nudge when a downstream app client is
|
|
941
|
+
// connected. Keeps the prompt clean when the agent has nowhere to render
|
|
942
|
+
// the tool's output, and keeps existing prompt-assembly tests stable
|
|
943
|
+
// (they exercise the prompt without spinning up an app client).
|
|
944
|
+
const hasAppClient =
|
|
945
|
+
server &&
|
|
946
|
+
typeof server.getConnectedAppCount === "function" &&
|
|
947
|
+
server.getConnectedAppCount() > 0;
|
|
948
|
+
const glassesUiNudge = hasAppClient ? composeGlassesUiNudgeSystemPrompt() : "";
|
|
949
|
+
const parts = [];
|
|
950
|
+
if (baseReadability) parts.push(baseReadability);
|
|
951
|
+
if (reactor) parts.push(reactor);
|
|
952
|
+
if (pace) parts.push(pace);
|
|
953
|
+
if (glassesUiNudge) parts.push(glassesUiNudge);
|
|
768
954
|
return {
|
|
769
|
-
extraSystemPrompt:
|
|
770
|
-
|
|
771
|
-
|
|
955
|
+
extraSystemPrompt: parts.join("\n\n"),
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function buildOcuClawSendDiagnostic(params = {}) {
|
|
960
|
+
const attachment = params.attachment || null;
|
|
961
|
+
const messageId =
|
|
962
|
+
typeof params.id === "string" && params.id.trim()
|
|
963
|
+
? params.id.trim()
|
|
964
|
+
: null;
|
|
965
|
+
const sessionKey =
|
|
966
|
+
typeof params.sessionKey === "string" && params.sessionKey.trim()
|
|
967
|
+
? params.sessionKey.trim()
|
|
968
|
+
: sessionService.ensureSessionKey();
|
|
969
|
+
const source =
|
|
970
|
+
typeof params.source === "string" && params.source.trim()
|
|
971
|
+
? params.source.trim()
|
|
972
|
+
: "relay_send";
|
|
973
|
+
|
|
974
|
+
return {
|
|
975
|
+
messageId,
|
|
976
|
+
sessionKey,
|
|
977
|
+
source,
|
|
978
|
+
textChars: typeof params.text === "string" ? params.text.length : 0,
|
|
979
|
+
hasAttachment: !!attachment,
|
|
980
|
+
attachmentBytes:
|
|
981
|
+
attachment && Number.isFinite(attachment.sizeBytes)
|
|
982
|
+
? Math.floor(attachment.sizeBytes)
|
|
983
|
+
: null,
|
|
772
984
|
};
|
|
773
985
|
}
|
|
774
986
|
|
|
@@ -815,6 +1027,9 @@ function createRelay(opts) {
|
|
|
815
1027
|
) {
|
|
816
1028
|
patch.thinkingLevel = settings.defaultThinking.trim().toLowerCase();
|
|
817
1029
|
}
|
|
1030
|
+
if (settings && settings.defaultFastMode === true) {
|
|
1031
|
+
patch.fastMode = true;
|
|
1032
|
+
}
|
|
818
1033
|
return Object.keys(patch).length > 0 ? patch : null;
|
|
819
1034
|
}
|
|
820
1035
|
|
|
@@ -905,8 +1120,92 @@ function createRelay(opts) {
|
|
|
905
1120
|
onSessionStateReset: resetActivityStatusAdapter,
|
|
906
1121
|
onPagesChanged: cachePages,
|
|
907
1122
|
onStatusChanged: broadcastStatus,
|
|
1123
|
+
onSessionModelConfig(config) {
|
|
1124
|
+
applyCurrentSessionModelConfigSnapshot(config);
|
|
1125
|
+
},
|
|
1126
|
+
broadcastSessions: () => broadcastSessions(),
|
|
1127
|
+
broadcastEvenAiSessions: () => broadcastEvenAiSessions(),
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
const relayHealth = createRelayHealthMonitor({
|
|
1131
|
+
emitDebug(event, severity, data) {
|
|
1132
|
+
emitDebug(
|
|
1133
|
+
"relay.health",
|
|
1134
|
+
event,
|
|
1135
|
+
severity,
|
|
1136
|
+
{ sessionKey: sessionService.peekSessionKey() || undefined },
|
|
1137
|
+
() => data,
|
|
1138
|
+
{ force: event === "relay_queue_depth" },
|
|
1139
|
+
);
|
|
1140
|
+
},
|
|
1141
|
+
});
|
|
1142
|
+
relayHealth.start();
|
|
1143
|
+
|
|
1144
|
+
const relayOperationRegistry = createRelayOperationRegistry({
|
|
1145
|
+
emitDebug(event, severity, data, context = {}) {
|
|
1146
|
+
emitDebug(
|
|
1147
|
+
"relay.operation",
|
|
1148
|
+
event,
|
|
1149
|
+
severity,
|
|
1150
|
+
{
|
|
1151
|
+
sessionKey: context.sessionKey || sessionService.peekSessionKey() || undefined,
|
|
1152
|
+
runId: context.runId || undefined,
|
|
1153
|
+
},
|
|
1154
|
+
() => data,
|
|
1155
|
+
);
|
|
1156
|
+
},
|
|
908
1157
|
});
|
|
909
1158
|
|
|
1159
|
+
function isActiveSessionModelConfig(config) {
|
|
1160
|
+
return !!(
|
|
1161
|
+
config &&
|
|
1162
|
+
typeof config.sessionKey === "string" &&
|
|
1163
|
+
(
|
|
1164
|
+
typeof sessionService.isCurrentSession === "function"
|
|
1165
|
+
? sessionService.isCurrentSession(config.sessionKey)
|
|
1166
|
+
: config.sessionKey === sessionService.ensureSessionKey()
|
|
1167
|
+
)
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function applyCurrentSessionModelConfigSnapshot(config) {
|
|
1172
|
+
if (!isActiveSessionModelConfig(config)) {
|
|
1173
|
+
return false;
|
|
1174
|
+
}
|
|
1175
|
+
currentSessionModelConfigSnapshot = config;
|
|
1176
|
+
if (
|
|
1177
|
+
upstreamRuntime &&
|
|
1178
|
+
typeof upstreamRuntime.handleCurrentSessionModelConfigChanged === "function"
|
|
1179
|
+
) {
|
|
1180
|
+
upstreamRuntime.handleCurrentSessionModelConfigChanged().catch((err) => {
|
|
1181
|
+
logger.warn(`[relay] Provider usage rebroadcast failed after session config update: ${err.message}`);
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
return true;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function clearCurrentSessionModelConfigSnapshot(trigger) {
|
|
1188
|
+
currentSessionModelConfigSnapshot = null;
|
|
1189
|
+
if (
|
|
1190
|
+
upstreamRuntime &&
|
|
1191
|
+
typeof upstreamRuntime.handleCurrentSessionModelConfigCleared === "function"
|
|
1192
|
+
) {
|
|
1193
|
+
upstreamRuntime.handleCurrentSessionModelConfigCleared().catch((err) => {
|
|
1194
|
+
logger.warn(`[relay] Provider usage clear broadcast failed after ${trigger}: ${err.message}`);
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// TTL fallback for set_session_title activity label. The tool itself
|
|
1200
|
+
// completes in <50ms but its label can linger if no follow-up activity
|
|
1201
|
+
// arrives (e.g. agent streams a response directly after, with no
|
|
1202
|
+
// intervening activity event). After 1s, synthesize a thinking-status
|
|
1203
|
+
// activity with no tool/label so the renderer falls back to the bare
|
|
1204
|
+
// animated spinner. Any real activity arriving in the meantime cancels
|
|
1205
|
+
// the timer.
|
|
1206
|
+
const SESSION_TITLE_STATUS_FALLBACK_MS = 1500;
|
|
1207
|
+
let sessionTitleStatusFallbackTimer = null;
|
|
1208
|
+
|
|
910
1209
|
function broadcastActivity(rawActivity) {
|
|
911
1210
|
const activity = activityStatusAdapter.augmentActivity(rawActivity || {});
|
|
912
1211
|
const runId = activity && activity.runId ? activity.runId : null;
|
|
@@ -928,6 +1227,8 @@ function createRelay(opts) {
|
|
|
928
1227
|
intent: (activity && activity.intent) || null,
|
|
929
1228
|
thinkingSummarySource: (activity && activity.thinkingSummarySource) || null,
|
|
930
1229
|
category: (activity && activity.category) || null,
|
|
1230
|
+
isError: typeof activity.isError === "boolean" ? activity.isError : null,
|
|
1231
|
+
code: (activity && activity.code) || null,
|
|
931
1232
|
activityId: (activity && activity.activityId) || null,
|
|
932
1233
|
seq: Number.isFinite(activity && activity.seq) ? activity.seq : null,
|
|
933
1234
|
origin,
|
|
@@ -936,9 +1237,194 @@ function createRelay(opts) {
|
|
|
936
1237
|
);
|
|
937
1238
|
|
|
938
1239
|
server.broadcast(handler.formatActivity(activity));
|
|
1240
|
+
|
|
1241
|
+
if (sessionTitleStatusFallbackTimer) {
|
|
1242
|
+
clearTimeout(sessionTitleStatusFallbackTimer);
|
|
1243
|
+
sessionTitleStatusFallbackTimer = null;
|
|
1244
|
+
}
|
|
1245
|
+
if (
|
|
1246
|
+
activity &&
|
|
1247
|
+
activity.tool === "set_session_title" &&
|
|
1248
|
+
phase !== "end" &&
|
|
1249
|
+
origin !== "synthetic_session_title_fallback"
|
|
1250
|
+
) {
|
|
1251
|
+
const fallbackSessionKey = activity.sessionKey || null;
|
|
1252
|
+
const fallbackRunId = runId;
|
|
1253
|
+
sessionTitleStatusFallbackTimer = setTimeout(() => {
|
|
1254
|
+
sessionTitleStatusFallbackTimer = null;
|
|
1255
|
+
broadcastActivity({
|
|
1256
|
+
state: "thinking",
|
|
1257
|
+
sessionKey: fallbackSessionKey,
|
|
1258
|
+
runId: fallbackRunId,
|
|
1259
|
+
origin: "synthetic_session_title_fallback",
|
|
1260
|
+
phase: "update",
|
|
1261
|
+
});
|
|
1262
|
+
}, SESSION_TITLE_STATUS_FALLBACK_MS);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
939
1265
|
return activity;
|
|
940
1266
|
}
|
|
941
1267
|
|
|
1268
|
+
function broadcastProviderUsageSnapshot(snapshot) {
|
|
1269
|
+
if (!server || !handler || typeof handler.formatProviderUsageSnapshot !== "function") {
|
|
1270
|
+
return snapshot;
|
|
1271
|
+
}
|
|
1272
|
+
server.broadcast(handler.formatProviderUsageSnapshot(snapshot || {}));
|
|
1273
|
+
return snapshot;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
const appClientDisconnectHandlers = new Set();
|
|
1277
|
+
function onAppClientDisconnect(handler) {
|
|
1278
|
+
if (typeof handler !== "function") return () => {};
|
|
1279
|
+
appClientDisconnectHandlers.add(handler);
|
|
1280
|
+
return () => appClientDisconnectHandlers.delete(handler);
|
|
1281
|
+
}
|
|
1282
|
+
function dispatchAppClientDisconnect(sessionKey) {
|
|
1283
|
+
for (const handler of appClientDisconnectHandlers) {
|
|
1284
|
+
try { handler({ sessionKey }); } catch (err) {
|
|
1285
|
+
logger.warn(`[relay] app_client_disconnect handler threw: ${err && err.message ? err.message : err}`);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
const glassesUiResultHandlers = new Set();
|
|
1291
|
+
|
|
1292
|
+
function sendGlassesUiRender(params) {
|
|
1293
|
+
if (!server) return;
|
|
1294
|
+
const payload = {
|
|
1295
|
+
type: "glasses_ui_render",
|
|
1296
|
+
sessionKey: params && typeof params.sessionKey === "string" ? params.sessionKey : null,
|
|
1297
|
+
surfaceId: params && typeof params.surfaceId === "string" ? params.surfaceId : "",
|
|
1298
|
+
depth: Number.isFinite(params && params.depth) ? Math.floor(params.depth) : 1,
|
|
1299
|
+
spec: params && params.spec ? params.spec : null,
|
|
1300
|
+
};
|
|
1301
|
+
server.broadcast(JSON.stringify(payload));
|
|
1302
|
+
emitDebug(
|
|
1303
|
+
"glasses.lifecycle",
|
|
1304
|
+
"surface_send",
|
|
1305
|
+
"debug",
|
|
1306
|
+
{ sessionKey: payload.sessionKey || undefined },
|
|
1307
|
+
() => ({ surfaceId: payload.surfaceId, mode: "render", depth: payload.depth, ...summarizeGlassesUiContent(payload.spec) }),
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function sendGlassesUiSurfaceUpdate(params) {
|
|
1312
|
+
if (!server) return;
|
|
1313
|
+
const patch = params && params.patch ? params.patch : null;
|
|
1314
|
+
if (!patch) return;
|
|
1315
|
+
const cleanPatch = {};
|
|
1316
|
+
if (typeof patch.title === "string") cleanPatch.title = patch.title;
|
|
1317
|
+
if (typeof patch.body === "string") cleanPatch.body = patch.body;
|
|
1318
|
+
if (Array.isArray(patch.items)) {
|
|
1319
|
+
// Items may be plain-string labels (list_surface / label-only) OR
|
|
1320
|
+
// {label, body} objects (list_with_details detail-body ticks). Keep both
|
|
1321
|
+
// shapes; drop anything malformed (no string, no string label).
|
|
1322
|
+
cleanPatch.items = patch.items
|
|
1323
|
+
.map((i) => {
|
|
1324
|
+
if (typeof i === "string") return i;
|
|
1325
|
+
if (i && typeof i === "object" && typeof i.label === "string") {
|
|
1326
|
+
const o = { label: i.label };
|
|
1327
|
+
if (typeof i.body === "string") o.body = i.body;
|
|
1328
|
+
return o;
|
|
1329
|
+
}
|
|
1330
|
+
return null;
|
|
1331
|
+
})
|
|
1332
|
+
.filter((i) => i !== null);
|
|
1333
|
+
}
|
|
1334
|
+
const payload = {
|
|
1335
|
+
type: "glasses_ui_surface_update",
|
|
1336
|
+
sessionKey: params && typeof params.sessionKey === "string" ? params.sessionKey : null,
|
|
1337
|
+
surfaceId: params && typeof params.surfaceId === "string" ? params.surfaceId : "",
|
|
1338
|
+
patch: cleanPatch,
|
|
1339
|
+
};
|
|
1340
|
+
server.broadcast(JSON.stringify(payload));
|
|
1341
|
+
emitDebug(
|
|
1342
|
+
"glasses.lifecycle",
|
|
1343
|
+
"surface_send",
|
|
1344
|
+
"debug",
|
|
1345
|
+
{ sessionKey: payload.sessionKey || undefined },
|
|
1346
|
+
() => ({ surfaceId: payload.surfaceId, mode: "update", ...summarizeGlassesUiContent(cleanPatch) }),
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
function onGlassesUiResult(handler) {
|
|
1351
|
+
if (typeof handler !== "function") return () => {};
|
|
1352
|
+
glassesUiResultHandlers.add(handler);
|
|
1353
|
+
return () => glassesUiResultHandlers.delete(handler);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function dispatchGlassesUiResult(frame) {
|
|
1357
|
+
if (!frame || typeof frame !== "object") return;
|
|
1358
|
+
for (const handler of glassesUiResultHandlers) {
|
|
1359
|
+
try {
|
|
1360
|
+
handler({
|
|
1361
|
+
surfaceId: typeof frame.surfaceId === "string" ? frame.surfaceId : "",
|
|
1362
|
+
outcome: frame.outcome,
|
|
1363
|
+
});
|
|
1364
|
+
} catch (err) {
|
|
1365
|
+
logger.warn(`[relay] glasses_ui_result handler threw: ${err.message}`);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
const glassesUiNavEventHandlers = new Set();
|
|
1371
|
+
|
|
1372
|
+
function onGlassesUiNavEvent(handler) {
|
|
1373
|
+
if (typeof handler !== "function") return () => {};
|
|
1374
|
+
glassesUiNavEventHandlers.add(handler);
|
|
1375
|
+
return () => glassesUiNavEventHandlers.delete(handler);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function dispatchGlassesUiNavEvent(frame) {
|
|
1379
|
+
if (!frame || typeof frame !== "object") return;
|
|
1380
|
+
for (const handler of glassesUiNavEventHandlers) {
|
|
1381
|
+
try {
|
|
1382
|
+
handler({
|
|
1383
|
+
surfaceId: typeof frame.surfaceId === "string" ? frame.surfaceId : "",
|
|
1384
|
+
depth: Number.isFinite(frame.depth) ? Math.max(1, Math.floor(frame.depth)) : 1,
|
|
1385
|
+
});
|
|
1386
|
+
} catch (err) {
|
|
1387
|
+
logger.warn(`[relay] glasses_ui_nav_event handler threw: ${err.message}`);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
const deviceInfoResponseHandlers = new Set();
|
|
1393
|
+
|
|
1394
|
+
function sendDeviceInfoRequest(params) {
|
|
1395
|
+
if (!server) return;
|
|
1396
|
+
const payload = {
|
|
1397
|
+
type: "device_info_request",
|
|
1398
|
+
sessionKey: params && typeof params.sessionKey === "string" ? params.sessionKey : null,
|
|
1399
|
+
requestId: params && typeof params.requestId === "string" ? params.requestId : "",
|
|
1400
|
+
};
|
|
1401
|
+
server.broadcast(JSON.stringify(payload));
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function onDeviceInfoResponse(handler) {
|
|
1405
|
+
if (typeof handler !== "function") return () => {};
|
|
1406
|
+
deviceInfoResponseHandlers.add(handler);
|
|
1407
|
+
return () => deviceInfoResponseHandlers.delete(handler);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
function dispatchDeviceInfoResponse(frame) {
|
|
1411
|
+
if (!frame || typeof frame !== "object") return;
|
|
1412
|
+
for (const handler of deviceInfoResponseHandlers) {
|
|
1413
|
+
try {
|
|
1414
|
+
handler({
|
|
1415
|
+
requestId: typeof frame.requestId === "string" ? frame.requestId : "",
|
|
1416
|
+
ok: frame.ok === true,
|
|
1417
|
+
code: typeof frame.code === "string" ? frame.code : undefined,
|
|
1418
|
+
data: frame.data && typeof frame.data === "object" ? frame.data : undefined,
|
|
1419
|
+
});
|
|
1420
|
+
} catch (err) {
|
|
1421
|
+
logger.warn(
|
|
1422
|
+
`[relay] device_info_response handler threw: ${err && err.message ? err.message : err}`,
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
942
1428
|
function normalizeAttachmentErrorCode(err) {
|
|
943
1429
|
if (!err) return "attachment_upstream_rejected";
|
|
944
1430
|
const code = typeof err.code === "string" ? err.code.trim() : "";
|
|
@@ -971,10 +1457,18 @@ function createRelay(opts) {
|
|
|
971
1457
|
const text = params.text;
|
|
972
1458
|
const sessionKey = params.sessionKey;
|
|
973
1459
|
const attachment = params.attachment || null;
|
|
1460
|
+
const clientDisplaySignals = params.clientDisplaySignals || null;
|
|
974
1461
|
const resolvedSessionKey = sessionKey || sessionService.ensureSessionKey();
|
|
975
1462
|
sessionService.recordFirstSentUserMessage(resolvedSessionKey, text);
|
|
1463
|
+
if (clientDisplaySignals && resolvedSessionKey) {
|
|
1464
|
+
sessionService.recordNeuralSessionNamesEnabled(
|
|
1465
|
+
resolvedSessionKey,
|
|
1466
|
+
clientDisplaySignals.neuralSessionNamesEnabled !== false,
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
976
1469
|
const hasAttachment = !!attachment;
|
|
977
1470
|
const sendStartedAt = Date.now();
|
|
1471
|
+
relayOperationRegistry.markStarted(id);
|
|
978
1472
|
sessionService.invalidateSessionsCache();
|
|
979
1473
|
emitDebug(
|
|
980
1474
|
"relay.protocol",
|
|
@@ -999,13 +1493,24 @@ function createRelay(opts) {
|
|
|
999
1493
|
text,
|
|
1000
1494
|
resolvedSessionKey,
|
|
1001
1495
|
attachment,
|
|
1002
|
-
|
|
1496
|
+
{
|
|
1497
|
+
...currentOcuClawSendOptions(clientDisplaySignals),
|
|
1498
|
+
diagnostic: buildOcuClawSendDiagnostic({
|
|
1499
|
+
...params,
|
|
1500
|
+
sessionKey: resolvedSessionKey,
|
|
1501
|
+
}),
|
|
1502
|
+
},
|
|
1003
1503
|
);
|
|
1004
1504
|
const upstreamDispatchedAt = Date.now();
|
|
1005
1505
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1506
|
+
const userContent = buildLocalUserMessageContent(text, attachment);
|
|
1507
|
+
conversationState.addMessage("user", userContent);
|
|
1508
|
+
emitDebug(
|
|
1509
|
+
"openclaw.message",
|
|
1510
|
+
"user_message",
|
|
1511
|
+
"info",
|
|
1512
|
+
{ sessionKey: resolvedSessionKey },
|
|
1513
|
+
() => ({ text: typeof text === "string" ? text : "" }),
|
|
1009
1514
|
);
|
|
1010
1515
|
broadcastPages();
|
|
1011
1516
|
const localPublishDoneAt = Date.now();
|
|
@@ -1028,6 +1533,10 @@ function createRelay(opts) {
|
|
|
1028
1533
|
(result) => {
|
|
1029
1534
|
const ackAt = Date.now();
|
|
1030
1535
|
const runId = result && result.runId ? result.runId : null;
|
|
1536
|
+
relayOperationRegistry.markUpstreamAck(id, {
|
|
1537
|
+
runId,
|
|
1538
|
+
status: result && result.status ? result.status : null,
|
|
1539
|
+
});
|
|
1031
1540
|
if (runId && upstreamRuntime) {
|
|
1032
1541
|
upstreamRuntime.trackAcceptedRun({
|
|
1033
1542
|
runId,
|
|
@@ -1053,8 +1562,16 @@ function createRelay(opts) {
|
|
|
1053
1562
|
return result;
|
|
1054
1563
|
},
|
|
1055
1564
|
(err) => {
|
|
1056
|
-
|
|
1057
|
-
err.errorCode
|
|
1565
|
+
const mirroredErrorCode =
|
|
1566
|
+
err && typeof err.errorCode === "string" && err.errorCode.trim()
|
|
1567
|
+
? err.errorCode.trim()
|
|
1568
|
+
: err && typeof err.code === "string" && err.code.trim()
|
|
1569
|
+
? err.code.trim()
|
|
1570
|
+
: attachment
|
|
1571
|
+
? normalizeAttachmentErrorCode(err)
|
|
1572
|
+
: null;
|
|
1573
|
+
if (mirroredErrorCode && err && typeof err === "object") {
|
|
1574
|
+
err.errorCode = mirroredErrorCode;
|
|
1058
1575
|
}
|
|
1059
1576
|
emitDebug(
|
|
1060
1577
|
"relay.protocol",
|
|
@@ -1065,7 +1582,8 @@ function createRelay(opts) {
|
|
|
1065
1582
|
messageId: id,
|
|
1066
1583
|
elapsedMs: Date.now() - sendStartedAt,
|
|
1067
1584
|
hasAttachment,
|
|
1068
|
-
errorCode:
|
|
1585
|
+
errorCode:
|
|
1586
|
+
err && typeof err.errorCode === "string" ? err.errorCode : null,
|
|
1069
1587
|
message: err && err.message ? err.message : String(err),
|
|
1070
1588
|
}),
|
|
1071
1589
|
);
|
|
@@ -1097,13 +1615,82 @@ function createRelay(opts) {
|
|
|
1097
1615
|
};
|
|
1098
1616
|
}
|
|
1099
1617
|
|
|
1618
|
+
function emitListenInterceptBroadcast(params = {}) {
|
|
1619
|
+
if (!server || !handler) {
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
const sessionKey = params && typeof params.sessionKey === "string" ? params.sessionKey : null;
|
|
1623
|
+
server.broadcast(handler.formatEvenAiListenIntercepted(sessionKey));
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1100
1626
|
// --- Downstream handler ---
|
|
1101
1627
|
|
|
1102
|
-
/** @type {ReturnType<typeof
|
|
1628
|
+
/** @type {ReturnType<typeof createRelayWorkerSupervisor>|null} */
|
|
1103
1629
|
let server = null;
|
|
1104
1630
|
let evenAiEndpoint = null;
|
|
1105
1631
|
let evenAiRouter = null;
|
|
1106
1632
|
let evenAiRunWaiter = null;
|
|
1633
|
+
const pendingBufferedEvenAiResponses = new Map();
|
|
1634
|
+
let relayApi = null;
|
|
1635
|
+
|
|
1636
|
+
function applyTraceLogSet(clientId, request) {
|
|
1637
|
+
const enabled = !!(request && request.enabled === true);
|
|
1638
|
+
liveUiTraceLogEnabled = enabled;
|
|
1639
|
+
let persisted = false;
|
|
1640
|
+
if (liveUiTraceFlagPath) {
|
|
1641
|
+
try {
|
|
1642
|
+
fs.writeFileSync(liveUiTraceFlagPath, JSON.stringify({ enabled }) + "\n");
|
|
1643
|
+
persisted = true;
|
|
1644
|
+
} catch (err) {
|
|
1645
|
+
logger.warn(`[relay] liveui trace-log flag persist failed: ${err && err.message ? err.message : err}`);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
emitDebug("relay.protocol", "trace_log_set", "info", { sessionKey: sessionService.ensureSessionKey() }, () => ({ clientId, enabled, persisted }));
|
|
1649
|
+
return { ok: true, enabled, persisted, persistedPath: liveUiTraceFlagPath };
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// Persist the current debug-store arm to debug-arm.json. Mirrors the
|
|
1653
|
+
// applyTraceLogSet writeFileSync above (plain, non-atomic): a partial/corrupt
|
|
1654
|
+
// write degrades to an empty arm on next boot — acceptable, the nothing-armed
|
|
1655
|
+
// warning catches it. getSnapshot().enabled is already pruned of expired
|
|
1656
|
+
// categories, so the persisted JSON never holds an expired entry. Never throws
|
|
1657
|
+
// into the caller.
|
|
1658
|
+
function persistDebugArm() {
|
|
1659
|
+
if (!debugArmStatePath) return false;
|
|
1660
|
+
try {
|
|
1661
|
+
const enabled = debugStore.getSnapshot().enabled;
|
|
1662
|
+
fs.writeFileSync(debugArmStatePath, JSON.stringify({ enabled }) + "\n");
|
|
1663
|
+
return true;
|
|
1664
|
+
} catch (err) {
|
|
1665
|
+
logger.warn(`[relay] debug arm persist failed: ${err && err.message ? err.message : err}`);
|
|
1666
|
+
return false;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
function applyDebugSet(clientId, request) {
|
|
1671
|
+
const result = debugStore.setCategories(request);
|
|
1672
|
+
if (!result.ok) {
|
|
1673
|
+
throw new Error(result.error || "debug-set failed");
|
|
1674
|
+
}
|
|
1675
|
+
// Persist after every successful set — enable AND disable-to-empty — so the
|
|
1676
|
+
// on-disk arm always tracks live state and a deliberately-cleared arm is not
|
|
1677
|
+
// resurrected on the next restart.
|
|
1678
|
+
persistDebugArm();
|
|
1679
|
+
emitDebug(
|
|
1680
|
+
"relay.protocol",
|
|
1681
|
+
"debug_set",
|
|
1682
|
+
"info",
|
|
1683
|
+
{ sessionKey: sessionService.ensureSessionKey() },
|
|
1684
|
+
() => ({
|
|
1685
|
+
clientId,
|
|
1686
|
+
enable: result.applied.enable,
|
|
1687
|
+
disable: result.applied.disable,
|
|
1688
|
+
ttlMs: result.ttlMs,
|
|
1689
|
+
enabledCount: result.enabled.length,
|
|
1690
|
+
}),
|
|
1691
|
+
);
|
|
1692
|
+
return result;
|
|
1693
|
+
}
|
|
1107
1694
|
|
|
1108
1695
|
const handler = createDownstreamHandler({
|
|
1109
1696
|
logger,
|
|
@@ -1122,14 +1709,99 @@ function createRelay(opts) {
|
|
|
1122
1709
|
* @param {object|null} attachment - Optional image attachment payload
|
|
1123
1710
|
* @returns {Promise}
|
|
1124
1711
|
*/
|
|
1125
|
-
onSend(id, text, sessionKey, attachment) {
|
|
1712
|
+
onSend(id, text, sessionKey, attachment, clientDisplaySignals) {
|
|
1126
1713
|
return dispatchOcuClawUserSend({
|
|
1127
1714
|
id,
|
|
1128
1715
|
text,
|
|
1129
1716
|
sessionKey,
|
|
1130
1717
|
attachment,
|
|
1718
|
+
clientDisplaySignals: clientDisplaySignals || null,
|
|
1719
|
+
source: "phone_ui",
|
|
1131
1720
|
});
|
|
1132
1721
|
},
|
|
1722
|
+
onGlassesUiResult(frame) {
|
|
1723
|
+
emitDebug(
|
|
1724
|
+
"glasses.lifecycle",
|
|
1725
|
+
"surface_outcome",
|
|
1726
|
+
"debug",
|
|
1727
|
+
{},
|
|
1728
|
+
() => ({ surfaceId: frame && frame.surfaceId, outcome: frame && frame.outcome }),
|
|
1729
|
+
);
|
|
1730
|
+
dispatchGlassesUiResult(frame);
|
|
1731
|
+
},
|
|
1732
|
+
onGlassesUiNavEvent(frame) {
|
|
1733
|
+
emitDebug(
|
|
1734
|
+
"glasses.lifecycle",
|
|
1735
|
+
"nav_event_recv",
|
|
1736
|
+
"debug",
|
|
1737
|
+
{},
|
|
1738
|
+
() => ({ surfaceId: frame && frame.surfaceId, depth: frame && frame.depth }),
|
|
1739
|
+
);
|
|
1740
|
+
dispatchGlassesUiNavEvent(frame);
|
|
1741
|
+
},
|
|
1742
|
+
onDeviceInfoResponse(frame) {
|
|
1743
|
+
dispatchDeviceInfoResponse(frame);
|
|
1744
|
+
},
|
|
1745
|
+
onGlassesUiRenderInject(params) {
|
|
1746
|
+
sendGlassesUiRender(params);
|
|
1747
|
+
},
|
|
1748
|
+
onSetUserSessionTitle(sessionKey, title) {
|
|
1749
|
+
const result = sessionService.setSessionTitle(sessionKey, title, { userSet: true });
|
|
1750
|
+
if (result && result.ok) {
|
|
1751
|
+
broadcastSessions();
|
|
1752
|
+
}
|
|
1753
|
+
},
|
|
1754
|
+
onSetSessionPinned(sessionKey, pinned, kind) {
|
|
1755
|
+
const result = sessionService.setSessionPinned(kind, sessionKey, pinned);
|
|
1756
|
+
if (result && result.ok) {
|
|
1757
|
+
broadcastSessions();
|
|
1758
|
+
}
|
|
1759
|
+
return result;
|
|
1760
|
+
},
|
|
1761
|
+
onCompactSession({ sessionKey }) {
|
|
1762
|
+
if (!upstreamRuntime || typeof upstreamRuntime.compactActiveSession !== "function") {
|
|
1763
|
+
return Promise.resolve({
|
|
1764
|
+
status: "rejected",
|
|
1765
|
+
error: "upstream runtime not ready",
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
return upstreamRuntime.compactActiveSession(sessionKey);
|
|
1769
|
+
},
|
|
1770
|
+
onDeleteSessions(sessionKeys, kind, switchBeforeDelete) {
|
|
1771
|
+
const action = switchBeforeDelete
|
|
1772
|
+
? sessionService.switchAndDeleteSessions(kind, sessionKeys)
|
|
1773
|
+
: sessionService.deleteSessions(kind, sessionKeys);
|
|
1774
|
+
Promise.resolve(action)
|
|
1775
|
+
.then(() => broadcastSessions())
|
|
1776
|
+
.catch((err) => {
|
|
1777
|
+
logger.error(`[relay] deleteSessions failed: ${err && err.message ? err.message : err}`);
|
|
1778
|
+
});
|
|
1779
|
+
},
|
|
1780
|
+
onSearchTranscripts(clientId, query, kind) {
|
|
1781
|
+
Promise.resolve(sessionService.searchTranscripts(kind, query))
|
|
1782
|
+
.then((result) => {
|
|
1783
|
+
if (!server) return;
|
|
1784
|
+
const payload = {
|
|
1785
|
+
type: "ocuclaw.session.transcripts.search.result",
|
|
1786
|
+
query,
|
|
1787
|
+
kind,
|
|
1788
|
+
snippets: result.snippets,
|
|
1789
|
+
truncated: result.truncated,
|
|
1790
|
+
};
|
|
1791
|
+
server.unicast(clientId, JSON.stringify(payload));
|
|
1792
|
+
})
|
|
1793
|
+
.catch((err) => {
|
|
1794
|
+
logger.error(`[relay] searchTranscripts failed: ${err && err.message ? err.message : err}`);
|
|
1795
|
+
if (server) {
|
|
1796
|
+
const payload = {
|
|
1797
|
+
type: "ocuclaw.session.transcripts.search.result",
|
|
1798
|
+
query, kind, snippets: [], truncated: false,
|
|
1799
|
+
};
|
|
1800
|
+
server.unicast(clientId, JSON.stringify(payload));
|
|
1801
|
+
}
|
|
1802
|
+
});
|
|
1803
|
+
},
|
|
1804
|
+
operationRegistry: relayOperationRegistry,
|
|
1133
1805
|
|
|
1134
1806
|
/**
|
|
1135
1807
|
* Inject a fake assistant message into conversation state.
|
|
@@ -1285,6 +1957,9 @@ function createRelay(opts) {
|
|
|
1285
1957
|
{ sessionKey: sessionService.ensureSessionKey() },
|
|
1286
1958
|
() => ({}),
|
|
1287
1959
|
);
|
|
1960
|
+
if (upstreamRuntime && typeof upstreamRuntime.clearTyping === "function") {
|
|
1961
|
+
upstreamRuntime.clearTyping("new_chat");
|
|
1962
|
+
}
|
|
1288
1963
|
sessionService.invalidateSessionsCache();
|
|
1289
1964
|
resetActivityStatusAdapter();
|
|
1290
1965
|
conversationState.clear();
|
|
@@ -1294,6 +1969,12 @@ function createRelay(opts) {
|
|
|
1294
1969
|
const pages = conversationState.getPages();
|
|
1295
1970
|
cachePages(pages);
|
|
1296
1971
|
if (upstreamRuntime && upstreamRuntime.isConnected()) {
|
|
1972
|
+
// NOTE: onNewChat targets the hard-coded "main" key (legacy) without
|
|
1973
|
+
// changing currentSessionKey, so it must NOT elicit a welcome turn here:
|
|
1974
|
+
// the turn's events would carry "main" and be dropped by isCurrentSession()
|
|
1975
|
+
// whenever the active session is an ocuclaw:* key. The welcome restore for
|
|
1976
|
+
// the real glasses paths lives in newSession() (New) and onSlashCommand
|
|
1977
|
+
// "/reset" (Reset). Unifying onNewChat onto newSession() is Phase-2 work.
|
|
1297
1978
|
gatewayBridge.sendMessage("/new", "main").catch((err) => {
|
|
1298
1979
|
logger.error(`[relay] Failed to send /new: ${err.message}`);
|
|
1299
1980
|
});
|
|
@@ -1307,6 +1988,10 @@ function createRelay(opts) {
|
|
|
1307
1988
|
|
|
1308
1989
|
onSwitchSession(sessionKey) {
|
|
1309
1990
|
return sessionService.switchToSession(sessionKey).then((pages) => {
|
|
1991
|
+
clearCurrentSessionModelConfigSnapshot("switch_session");
|
|
1992
|
+
if (upstreamRuntime && typeof upstreamRuntime.clearTyping === "function") {
|
|
1993
|
+
upstreamRuntime.clearTyping("switch_session");
|
|
1994
|
+
}
|
|
1310
1995
|
if (upstreamRuntime && typeof upstreamRuntime.handleSessionChanged === "function") {
|
|
1311
1996
|
upstreamRuntime.handleSessionChanged("switch_session");
|
|
1312
1997
|
}
|
|
@@ -1316,6 +2001,10 @@ function createRelay(opts) {
|
|
|
1316
2001
|
|
|
1317
2002
|
async onNewSession() {
|
|
1318
2003
|
const result = await sessionService.newSession();
|
|
2004
|
+
clearCurrentSessionModelConfigSnapshot("new_session");
|
|
2005
|
+
if (upstreamRuntime && typeof upstreamRuntime.clearTyping === "function") {
|
|
2006
|
+
upstreamRuntime.clearTyping("new_session");
|
|
2007
|
+
}
|
|
1319
2008
|
if (upstreamRuntime && typeof upstreamRuntime.handleSessionChanged === "function") {
|
|
1320
2009
|
upstreamRuntime.handleSessionChanged("new_session");
|
|
1321
2010
|
}
|
|
@@ -1342,6 +2031,20 @@ function createRelay(opts) {
|
|
|
1342
2031
|
: Promise.resolve({ skills: [], fetchedAtMs: Date.now(), stale: true });
|
|
1343
2032
|
},
|
|
1344
2033
|
|
|
2034
|
+
onGetProviderUsageSnapshot() {
|
|
2035
|
+
return upstreamRuntime
|
|
2036
|
+
? upstreamRuntime.getProviderUsageSnapshot()
|
|
2037
|
+
: Promise.resolve({
|
|
2038
|
+
sessionKey: null,
|
|
2039
|
+
provider: null,
|
|
2040
|
+
displayName: null,
|
|
2041
|
+
limitingWindowKey: null,
|
|
2042
|
+
windows: [],
|
|
2043
|
+
fetchedAtMs: Date.now(),
|
|
2044
|
+
stale: true,
|
|
2045
|
+
});
|
|
2046
|
+
},
|
|
2047
|
+
|
|
1345
2048
|
onGetSonioxModels() {
|
|
1346
2049
|
return getSonioxModelsSnapshot();
|
|
1347
2050
|
},
|
|
@@ -1356,7 +2059,13 @@ function createRelay(opts) {
|
|
|
1356
2059
|
|
|
1357
2060
|
async onSetSessionModelConfig(patch) {
|
|
1358
2061
|
const result = await sessionService.setCurrentSessionModelConfig(patch || {});
|
|
1359
|
-
if (
|
|
2062
|
+
if (
|
|
2063
|
+
result &&
|
|
2064
|
+
result.status === "accepted" &&
|
|
2065
|
+
result.config &&
|
|
2066
|
+
isActiveSessionModelConfig(result.config)
|
|
2067
|
+
) {
|
|
2068
|
+
currentSessionModelConfigSnapshot = result.config;
|
|
1360
2069
|
server.broadcast(handler.formatSessionModelConfig(result.config));
|
|
1361
2070
|
}
|
|
1362
2071
|
return result;
|
|
@@ -1371,50 +2080,7 @@ function createRelay(opts) {
|
|
|
1371
2080
|
},
|
|
1372
2081
|
|
|
1373
2082
|
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
|
-
};
|
|
2083
|
+
return buildEvenAiSessionsSnapshot();
|
|
1418
2084
|
},
|
|
1419
2085
|
|
|
1420
2086
|
async onSetEvenAiSettings(patch) {
|
|
@@ -1445,13 +2111,26 @@ function createRelay(opts) {
|
|
|
1445
2111
|
sessionService.invalidateSessionsCache();
|
|
1446
2112
|
resetActivityStatusAdapter();
|
|
1447
2113
|
conversationState.clear();
|
|
2114
|
+
if (upstreamRuntime && typeof upstreamRuntime.clearTyping === "function") {
|
|
2115
|
+
upstreamRuntime.clearTyping("slash_reset");
|
|
2116
|
+
}
|
|
1448
2117
|
conversationState.setAgentName(
|
|
1449
2118
|
(upstreamRuntime ? upstreamRuntime.getAgentName() : null) || "Agent",
|
|
1450
2119
|
);
|
|
1451
2120
|
broadcastPages();
|
|
1452
2121
|
}
|
|
1453
2122
|
if (upstreamRuntime && upstreamRuntime.isConnected()) {
|
|
1454
|
-
|
|
2123
|
+
// Bare /reset no longer elicits an agent turn on OpenClaw 2026.6.x
|
|
2124
|
+
// (fast-reset). Append the greeting prompt so Reset gets the same
|
|
2125
|
+
// welcome as New. Other slash commands forward verbatim.
|
|
2126
|
+
const outboundCommand =
|
|
2127
|
+
command === "/reset"
|
|
2128
|
+
? `/reset ${NEW_SESSION_GREETING_PROMPT}`
|
|
2129
|
+
: command;
|
|
2130
|
+
return gatewayBridge.sendMessage(
|
|
2131
|
+
outboundCommand,
|
|
2132
|
+
sessionService.ensureSessionKey(),
|
|
2133
|
+
);
|
|
1455
2134
|
}
|
|
1456
2135
|
return Promise.resolve();
|
|
1457
2136
|
},
|
|
@@ -1460,7 +2139,7 @@ function createRelay(opts) {
|
|
|
1460
2139
|
* @returns {boolean} Whether upstream is connected.
|
|
1461
2140
|
*/
|
|
1462
2141
|
isUpstreamConnected() {
|
|
1463
|
-
return
|
|
2142
|
+
return true;
|
|
1464
2143
|
},
|
|
1465
2144
|
|
|
1466
2145
|
onConsoleLog(level, message) {
|
|
@@ -1510,26 +2189,14 @@ function createRelay(opts) {
|
|
|
1510
2189
|
},
|
|
1511
2190
|
|
|
1512
2191
|
onDebugSet(clientId, request) {
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
throw new Error(result.error || "debug-set failed");
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
emitDebug(
|
|
1519
|
-
"relay.protocol",
|
|
1520
|
-
"debug_set",
|
|
1521
|
-
"info",
|
|
1522
|
-
{ sessionKey: sessionService.ensureSessionKey() },
|
|
1523
|
-
() => ({
|
|
1524
|
-
clientId,
|
|
1525
|
-
enable: result.applied.enable,
|
|
1526
|
-
disable: result.applied.disable,
|
|
1527
|
-
ttlMs: result.ttlMs,
|
|
1528
|
-
enabledCount: result.enabled.length,
|
|
1529
|
-
}),
|
|
1530
|
-
);
|
|
2192
|
+
return applyDebugSet(clientId, request);
|
|
2193
|
+
},
|
|
1531
2194
|
|
|
1532
|
-
|
|
2195
|
+
onTraceLogSet(clientId, request) {
|
|
2196
|
+
return applyTraceLogSet(clientId, request);
|
|
2197
|
+
},
|
|
2198
|
+
onTraceLogGet() {
|
|
2199
|
+
return { ok: true, enabled: liveUiTraceLogEnabled, persistedPath: liveUiTraceFlagPath };
|
|
1533
2200
|
},
|
|
1534
2201
|
|
|
1535
2202
|
onDebugDump(clientId, request) {
|
|
@@ -1766,14 +2433,153 @@ function createRelay(opts) {
|
|
|
1766
2433
|
},
|
|
1767
2434
|
};
|
|
1768
2435
|
},
|
|
2436
|
+
|
|
2437
|
+
onAutomationState(clientId, request) {
|
|
2438
|
+
// Mirrors onReadinessProbe (above): identify the single connected app
|
|
2439
|
+
// client via the readiness snapshot, then return a dispatch envelope
|
|
2440
|
+
// that downstream-handler.handleAutomationState wraps into
|
|
2441
|
+
// `automationStateRequest`. Without this callback wired, the handler
|
|
2442
|
+
// returns null and the request is silently dropped at the relay —
|
|
2443
|
+
// simctl/debugctl times out with no failure response, no trace event,
|
|
2444
|
+
// no outbox drop. The lack of wiring was found 2026-05-28 while
|
|
2445
|
+
// validating the streaming-thinking-emoji-demotion fix on the sim.
|
|
2446
|
+
const now = Date.now();
|
|
2447
|
+
const requestId =
|
|
2448
|
+
(typeof request.requestId === "string" && request.requestId.trim()) ||
|
|
2449
|
+
`automation-${now}-${Math.random().toString(16).slice(2, 8)}`;
|
|
2450
|
+
const requestedSessionKey =
|
|
2451
|
+
typeof request.sessionKey === "string" && request.sessionKey.trim()
|
|
2452
|
+
? request.sessionKey.trim()
|
|
2453
|
+
: null;
|
|
2454
|
+
const snapshot =
|
|
2455
|
+
server && typeof server.getReadinessSnapshot === "function"
|
|
2456
|
+
? server.getReadinessSnapshot()
|
|
2457
|
+
: {
|
|
2458
|
+
connectedClientCount: 0,
|
|
2459
|
+
fanoutRecipientCount: 0,
|
|
2460
|
+
clients: [],
|
|
2461
|
+
};
|
|
2462
|
+
const targetEntry =
|
|
2463
|
+
snapshot &&
|
|
2464
|
+
snapshot.connectedClientCount === 1 &&
|
|
2465
|
+
snapshot.fanoutRecipientCount === 1 &&
|
|
2466
|
+
Array.isArray(snapshot.clients) &&
|
|
2467
|
+
snapshot.clients.length === 1
|
|
2468
|
+
? snapshot.clients[0]
|
|
2469
|
+
: null;
|
|
2470
|
+
const targetClientId =
|
|
2471
|
+
targetEntry && typeof targetEntry.clientId === "string"
|
|
2472
|
+
? targetEntry.clientId
|
|
2473
|
+
: null;
|
|
2474
|
+
// A connected app client that has never published a readiness snapshot
|
|
2475
|
+
// cannot answer an automation state request; forwarding anyway would
|
|
2476
|
+
// park the request in pendingAutomationStateRequests with no reply.
|
|
2477
|
+
// Same predicate as the downstream readiness gate; this wired callback
|
|
2478
|
+
// bypasses the normal dispatch path.
|
|
2479
|
+
const readinessPublished =
|
|
2480
|
+
!!(
|
|
2481
|
+
targetEntry &&
|
|
2482
|
+
targetEntry.readinessSnapshot &&
|
|
2483
|
+
Number.isFinite(targetEntry.readinessSnapshot.emittedAtMs)
|
|
2484
|
+
);
|
|
2485
|
+
|
|
2486
|
+
emitDebug(
|
|
2487
|
+
"relay.protocol",
|
|
2488
|
+
"automation_state_requested",
|
|
2489
|
+
"info",
|
|
2490
|
+
{ sessionKey: sessionService.ensureSessionKey() },
|
|
2491
|
+
() => ({
|
|
2492
|
+
clientId,
|
|
2493
|
+
requestId,
|
|
2494
|
+
requestedSessionKey,
|
|
2495
|
+
connectedClientCount:
|
|
2496
|
+
snapshot && Number.isFinite(snapshot.connectedClientCount)
|
|
2497
|
+
? snapshot.connectedClientCount
|
|
2498
|
+
: 0,
|
|
2499
|
+
fanoutRecipientCount:
|
|
2500
|
+
snapshot && Number.isFinite(snapshot.fanoutRecipientCount)
|
|
2501
|
+
? snapshot.fanoutRecipientCount
|
|
2502
|
+
: 0,
|
|
2503
|
+
}),
|
|
2504
|
+
);
|
|
2505
|
+
|
|
2506
|
+
if (
|
|
2507
|
+
!snapshot ||
|
|
2508
|
+
snapshot.connectedClientCount <= 0 ||
|
|
2509
|
+
snapshot.fanoutRecipientCount <= 0
|
|
2510
|
+
) {
|
|
2511
|
+
return {
|
|
2512
|
+
ok: false,
|
|
2513
|
+
requestId,
|
|
2514
|
+
reasonCode: "no_downstream_client",
|
|
2515
|
+
message: "No downstream app clients connected",
|
|
2516
|
+
};
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
if (
|
|
2520
|
+
snapshot.connectedClientCount > 1 ||
|
|
2521
|
+
snapshot.fanoutRecipientCount > 1 ||
|
|
2522
|
+
!targetClientId
|
|
2523
|
+
) {
|
|
2524
|
+
return {
|
|
2525
|
+
ok: false,
|
|
2526
|
+
requestId,
|
|
2527
|
+
reasonCode: "multi_recipient_fanout",
|
|
2528
|
+
message: "Multiple downstream app clients connected",
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
if (!readinessPublished) {
|
|
2533
|
+
return {
|
|
2534
|
+
ok: false,
|
|
2535
|
+
requestId,
|
|
2536
|
+
reasonCode: "snapshot_unavailable",
|
|
2537
|
+
message: "Automation state snapshot is unavailable",
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
emitDebug(
|
|
2542
|
+
"relay.protocol",
|
|
2543
|
+
"automation_state_dispatched",
|
|
2544
|
+
"info",
|
|
2545
|
+
{ sessionKey: sessionService.ensureSessionKey() },
|
|
2546
|
+
() => ({
|
|
2547
|
+
clientId,
|
|
2548
|
+
requestId,
|
|
2549
|
+
targetClientId,
|
|
2550
|
+
}),
|
|
2551
|
+
);
|
|
2552
|
+
|
|
2553
|
+
return {
|
|
2554
|
+
ok: true,
|
|
2555
|
+
requestId,
|
|
2556
|
+
targetClientId,
|
|
2557
|
+
request: {
|
|
2558
|
+
requestId,
|
|
2559
|
+
sessionKey: requestedSessionKey,
|
|
2560
|
+
},
|
|
2561
|
+
};
|
|
2562
|
+
},
|
|
1769
2563
|
});
|
|
1770
2564
|
|
|
1771
|
-
// ---
|
|
2565
|
+
// --- Worker supervisor ---
|
|
2566
|
+
|
|
2567
|
+
const pluginVersionService = createPluginVersionService();
|
|
1772
2568
|
|
|
1773
|
-
server =
|
|
2569
|
+
server = createRelayWorkerSupervisor({
|
|
2570
|
+
pluginId: "ocuclaw",
|
|
2571
|
+
getPluginVersion: () => pluginVersionService.getPluginVersion(),
|
|
2572
|
+
getRequiresClientVersion: () => pluginVersionService.getRequiresClientVersion(),
|
|
1774
2573
|
logger,
|
|
1775
|
-
externalDebugToolsEnabled,
|
|
1776
2574
|
handler,
|
|
2575
|
+
operationRegistry: relayOperationRegistry,
|
|
2576
|
+
host: opts.host,
|
|
2577
|
+
port: opts.port,
|
|
2578
|
+
token: opts.token,
|
|
2579
|
+
externalDebugToolsEnabled,
|
|
2580
|
+
evenAiRequestTimeoutMs: opts.evenAiRequestTimeoutMs,
|
|
2581
|
+
evenAiMaxBodyBytes: opts.evenAiMaxBodyBytes,
|
|
2582
|
+
evenAiMaxResponseBytes: opts.evenAiMaxResponseBytes,
|
|
1777
2583
|
getCurrentPages() {
|
|
1778
2584
|
return cachedPages;
|
|
1779
2585
|
},
|
|
@@ -1789,82 +2595,30 @@ function createRelay(opts) {
|
|
|
1789
2595
|
statusRevision: statusRevision || 0,
|
|
1790
2596
|
};
|
|
1791
2597
|
},
|
|
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
|
-
);
|
|
2598
|
+
getAgentAvatarHash: () =>
|
|
2599
|
+
upstreamRuntime && typeof upstreamRuntime.getAgentAvatarHash === "function"
|
|
2600
|
+
? upstreamRuntime.getAgentAvatarHash()
|
|
2601
|
+
: null,
|
|
2602
|
+
getAgentAvatarDataUriByHash: (hash) =>
|
|
2603
|
+
upstreamRuntime && typeof upstreamRuntime.getAgentAvatarDataUriByHash === "function"
|
|
2604
|
+
? upstreamRuntime.getAgentAvatarDataUriByHash(hash)
|
|
2605
|
+
: null,
|
|
2606
|
+
handleBufferedEvenAiHttpRequest(envelope) {
|
|
2607
|
+
return handleBufferedEvenAiHttpRequest(envelope);
|
|
1806
2608
|
},
|
|
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
|
-
);
|
|
2609
|
+
cancelBufferedEvenAiHttpRequest(envelope) {
|
|
2610
|
+
return cancelBufferedEvenAiHttpRequest(envelope);
|
|
1832
2611
|
},
|
|
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
|
-
);
|
|
2612
|
+
getActiveSessionKey() {
|
|
2613
|
+
return sessionService.peekSessionKey() || null;
|
|
2614
|
+
},
|
|
2615
|
+
onAppClientDisconnect(sessionKey) {
|
|
2616
|
+
dispatchAppClientDisconnect(sessionKey);
|
|
2617
|
+
},
|
|
2618
|
+
emitDebug(category, event, severity, context, payloadFactory, options) {
|
|
2619
|
+
emitDebug(category, event, severity, context, payloadFactory, options);
|
|
1853
2620
|
},
|
|
1854
|
-
httpServer: sharedHttpServer,
|
|
1855
|
-
port: opts.port,
|
|
1856
|
-
host: opts.host,
|
|
1857
|
-
token: opts.token,
|
|
1858
2621
|
});
|
|
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
2622
|
|
|
1869
2623
|
// --- Helpers ---
|
|
1870
2624
|
|
|
@@ -1876,6 +2630,8 @@ function createRelay(opts) {
|
|
|
1876
2630
|
? "connected"
|
|
1877
2631
|
: "disconnected",
|
|
1878
2632
|
agent: upstreamRuntime ? upstreamRuntime.getAgentName() : null,
|
|
2633
|
+
agentEmoji: upstreamRuntime ? upstreamRuntime.getAgentEmoji() : null,
|
|
2634
|
+
agentAvatarHash: upstreamRuntime ? upstreamRuntime.getAgentAvatarHash() : null,
|
|
1879
2635
|
session: sessionService.ensureSessionKey(),
|
|
1880
2636
|
evenAiEnabled: opts.evenAiEnabled === true,
|
|
1881
2637
|
};
|
|
@@ -1924,6 +2680,97 @@ function createRelay(opts) {
|
|
|
1924
2680
|
}
|
|
1925
2681
|
}
|
|
1926
2682
|
|
|
2683
|
+
/**
|
|
2684
|
+
* Fetch the latest sessions snapshot and broadcast it. Used after a session
|
|
2685
|
+
* title changes so connected clients refresh the title in the main webui
|
|
2686
|
+
* status row and Session Settings tab without waiting for a manual
|
|
2687
|
+
* session-list open.
|
|
2688
|
+
*/
|
|
2689
|
+
function broadcastSessions() {
|
|
2690
|
+
sessionService
|
|
2691
|
+
.getSessions()
|
|
2692
|
+
.then((sessions) => {
|
|
2693
|
+
server.broadcast(handler.formatSessions(sessions));
|
|
2694
|
+
})
|
|
2695
|
+
.catch((err) => {
|
|
2696
|
+
emitDebug(
|
|
2697
|
+
"relay.session",
|
|
2698
|
+
"session_broadcast_failed",
|
|
2699
|
+
"debug",
|
|
2700
|
+
{ sessionKey: sessionService.peekSessionKey() || undefined },
|
|
2701
|
+
() => ({ message: err && err.message ? err.message : String(err) }),
|
|
2702
|
+
);
|
|
2703
|
+
});
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
/**
|
|
2707
|
+
* Resolve the current Even AI sessions snapshot for unicast/broadcast.
|
|
2708
|
+
* Mirrors the shape that `formatEvenAiSessions` expects.
|
|
2709
|
+
*/
|
|
2710
|
+
async function buildEvenAiSessionsSnapshot() {
|
|
2711
|
+
const dedicatedKey =
|
|
2712
|
+
evenAiRouter && typeof evenAiRouter.getDedicatedSessionKey === "function"
|
|
2713
|
+
? evenAiRouter.getDedicatedSessionKey()
|
|
2714
|
+
: opts.evenAiDedicatedSessionKey;
|
|
2715
|
+
const dedicatedEvenAiKey = normalizeEvenAiSessionKeyForLookup(dedicatedKey);
|
|
2716
|
+
const trackedThrowawayKeys =
|
|
2717
|
+
typeof evenAiSettingsStore.getTrackedThrowawayKeys === "function"
|
|
2718
|
+
? evenAiSettingsStore.getTrackedThrowawayKeys()
|
|
2719
|
+
: [];
|
|
2720
|
+
const normalizedTrackedThrowawayKeys = dedupeNormalizedSessionKeys(
|
|
2721
|
+
trackedThrowawayKeys,
|
|
2722
|
+
);
|
|
2723
|
+
const resolvedSessions = await sessionService.getSessionsByExactKeys([
|
|
2724
|
+
...normalizedTrackedThrowawayKeys,
|
|
2725
|
+
...(dedicatedEvenAiKey ? [dedicatedEvenAiKey] : []),
|
|
2726
|
+
]);
|
|
2727
|
+
const normalizedDedicatedKey = dedicatedEvenAiKey.toLowerCase();
|
|
2728
|
+
const sessions = [];
|
|
2729
|
+
let dedicatedIncluded = false;
|
|
2730
|
+
for (const session of resolvedSessions) {
|
|
2731
|
+
if (
|
|
2732
|
+
!dedicatedIncluded &&
|
|
2733
|
+
session &&
|
|
2734
|
+
typeof session.key === "string" &&
|
|
2735
|
+
session.key.trim().toLowerCase() === normalizedDedicatedKey
|
|
2736
|
+
) {
|
|
2737
|
+
sessions.push(session);
|
|
2738
|
+
dedicatedIncluded = true;
|
|
2739
|
+
continue;
|
|
2740
|
+
}
|
|
2741
|
+
sessions.push(session);
|
|
2742
|
+
}
|
|
2743
|
+
if (!dedicatedIncluded && dedicatedEvenAiKey) {
|
|
2744
|
+
sessions.unshift({
|
|
2745
|
+
key: dedicatedEvenAiKey,
|
|
2746
|
+
updatedAt: 0,
|
|
2747
|
+
preview: "",
|
|
2748
|
+
firstUserMessage: "",
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
return { sessions, dedicatedKey };
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
function broadcastEvenAiSessions() {
|
|
2755
|
+
if (!server) return;
|
|
2756
|
+
buildEvenAiSessionsSnapshot()
|
|
2757
|
+
.then((payload) => {
|
|
2758
|
+
server.broadcast(handler.formatEvenAiSessions(payload));
|
|
2759
|
+
})
|
|
2760
|
+
.catch((err) => {
|
|
2761
|
+
emitDebug(
|
|
2762
|
+
"relay.session",
|
|
2763
|
+
"session_broadcast_failed",
|
|
2764
|
+
"debug",
|
|
2765
|
+
{ sessionKey: sessionService.peekSessionKey() || undefined },
|
|
2766
|
+
() => ({
|
|
2767
|
+
kind: "evenai",
|
|
2768
|
+
message: err && err.message ? err.message : String(err),
|
|
2769
|
+
}),
|
|
2770
|
+
);
|
|
2771
|
+
});
|
|
2772
|
+
}
|
|
2773
|
+
|
|
1927
2774
|
/**
|
|
1928
2775
|
* Build, cache, and broadcast the current status.
|
|
1929
2776
|
*/
|
|
@@ -1932,10 +2779,24 @@ function createRelay(opts) {
|
|
|
1932
2779
|
if (next !== null) {
|
|
1933
2780
|
server.broadcast(next);
|
|
1934
2781
|
}
|
|
2782
|
+
if (server && typeof server.notifyAgentAvatarChanged === "function") {
|
|
2783
|
+
const hash =
|
|
2784
|
+
upstreamRuntime && typeof upstreamRuntime.getAgentAvatarHash === "function"
|
|
2785
|
+
? upstreamRuntime.getAgentAvatarHash()
|
|
2786
|
+
: null;
|
|
2787
|
+
const dataUri =
|
|
2788
|
+
hash &&
|
|
2789
|
+
upstreamRuntime &&
|
|
2790
|
+
typeof upstreamRuntime.getAgentAvatarDataUriByHash === "function"
|
|
2791
|
+
? upstreamRuntime.getAgentAvatarDataUriByHash(hash)
|
|
2792
|
+
: null;
|
|
2793
|
+
server.notifyAgentAvatarChanged(hash, dataUri);
|
|
2794
|
+
}
|
|
1935
2795
|
}
|
|
1936
2796
|
|
|
1937
2797
|
upstreamRuntime = createUpstreamRuntime({
|
|
1938
2798
|
logger,
|
|
2799
|
+
stateDir: opts.stateDir,
|
|
1939
2800
|
gatewayBridge,
|
|
1940
2801
|
conversationState,
|
|
1941
2802
|
sessionService,
|
|
@@ -1944,6 +2805,11 @@ function createRelay(opts) {
|
|
|
1944
2805
|
broadcastPages,
|
|
1945
2806
|
broadcastStatus,
|
|
1946
2807
|
broadcastActivity,
|
|
2808
|
+
broadcastProviderUsageSnapshot,
|
|
2809
|
+
operationRegistry: relayOperationRegistry,
|
|
2810
|
+
getCurrentSessionModelConfigSnapshot() {
|
|
2811
|
+
return currentSessionModelConfigSnapshot;
|
|
2812
|
+
},
|
|
1947
2813
|
resetActivityStatusAdapter,
|
|
1948
2814
|
modelsCacheTtlMs: opts.modelsCacheTtlMs,
|
|
1949
2815
|
getServer() {
|
|
@@ -1952,8 +2818,39 @@ function createRelay(opts) {
|
|
|
1952
2818
|
getVoiceRuntime() {
|
|
1953
2819
|
return null;
|
|
1954
2820
|
},
|
|
2821
|
+
gatewayUrl: opts.gatewayUrl,
|
|
2822
|
+
gatewayToken: opts.gatewayToken,
|
|
2823
|
+
fetchAgentAvatar: opts.fetchAgentAvatar,
|
|
1955
2824
|
});
|
|
1956
2825
|
|
|
2826
|
+
// Shared routing gate for session-scoped Even AI defaults (thinking seed,
|
|
2827
|
+
// fast-mode patch): never touch active-routed sessions; always seed fresh
|
|
2828
|
+
// background_new sessions; seed persistent background sessions only before
|
|
2829
|
+
// their first turn exists.
|
|
2830
|
+
async function shouldSeedSessionScopedDefaultForRoute(route) {
|
|
2831
|
+
const routingMode =
|
|
2832
|
+
route && typeof route.routingMode === "string"
|
|
2833
|
+
? route.routingMode.trim().toLowerCase()
|
|
2834
|
+
: "active";
|
|
2835
|
+
const sessionKey =
|
|
2836
|
+
route && typeof route.sessionKey === "string" ? route.sessionKey.trim() : "";
|
|
2837
|
+
if (!sessionKey || routingMode === "active") {
|
|
2838
|
+
return false;
|
|
2839
|
+
}
|
|
2840
|
+
if (routingMode === "background_new") {
|
|
2841
|
+
return true;
|
|
2842
|
+
}
|
|
2843
|
+
if (routingMode !== "background") {
|
|
2844
|
+
return false;
|
|
2845
|
+
}
|
|
2846
|
+
try {
|
|
2847
|
+
const existingSessions = await sessionService.getSessionsByExactKeys([sessionKey]);
|
|
2848
|
+
return existingSessions.length === 0;
|
|
2849
|
+
} catch {
|
|
2850
|
+
return false;
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
|
|
1957
2854
|
if (opts.evenAiEnabled === true) {
|
|
1958
2855
|
evenAiRouter = createEvenAiRouter({
|
|
1959
2856
|
sessionService,
|
|
@@ -1971,7 +2868,7 @@ function createRelay(opts) {
|
|
|
1971
2868
|
logger,
|
|
1972
2869
|
httpServer: sharedHttpServer,
|
|
1973
2870
|
enabled: true,
|
|
1974
|
-
externallyRouted:
|
|
2871
|
+
externallyRouted: true,
|
|
1975
2872
|
token: opts.evenAiToken,
|
|
1976
2873
|
getSettingsSnapshot() {
|
|
1977
2874
|
return evenAiSettingsStore.getSnapshot();
|
|
@@ -1992,6 +2889,9 @@ function createRelay(opts) {
|
|
|
1992
2889
|
emitListenInterceptRecovery(params) {
|
|
1993
2890
|
return emitListenInterceptRecovery(params);
|
|
1994
2891
|
},
|
|
2892
|
+
emitListenInterceptBroadcast(params) {
|
|
2893
|
+
return emitListenInterceptBroadcast(params);
|
|
2894
|
+
},
|
|
1995
2895
|
hasConnectedAppClient() {
|
|
1996
2896
|
return server ? server.getConnectedAppCount() > 0 : false;
|
|
1997
2897
|
},
|
|
@@ -2011,31 +2911,29 @@ function createRelay(opts) {
|
|
|
2011
2911
|
},
|
|
2012
2912
|
async shouldSeedThinkingForRoute(params) {
|
|
2013
2913
|
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
2914
|
const thinkingLevel =
|
|
2019
2915
|
params && typeof params.thinkingLevel === "string"
|
|
2020
2916
|
? params.thinkingLevel.trim().toLowerCase()
|
|
2021
2917
|
: "";
|
|
2022
|
-
|
|
2023
|
-
route && typeof route.sessionKey === "string" ? route.sessionKey.trim() : "";
|
|
2024
|
-
if (!thinkingLevel || !sessionKey || routingMode === "active") {
|
|
2918
|
+
if (!thinkingLevel) {
|
|
2025
2919
|
return false;
|
|
2026
2920
|
}
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2921
|
+
return shouldSeedSessionScopedDefaultForRoute(route);
|
|
2922
|
+
},
|
|
2923
|
+
async seedFastModeForRoute(params) {
|
|
2924
|
+
const route = params && params.route ? params.route : params;
|
|
2925
|
+
const settings = evenAiSettingsStore.getSnapshot();
|
|
2926
|
+
if (!settings || settings.defaultFastMode !== true) {
|
|
2031
2927
|
return false;
|
|
2032
2928
|
}
|
|
2033
|
-
|
|
2034
|
-
const existingSessions = await sessionService.getSessionsByExactKeys([sessionKey]);
|
|
2035
|
-
return existingSessions.length === 0;
|
|
2036
|
-
} catch {
|
|
2929
|
+
if (!(await shouldSeedSessionScopedDefaultForRoute(route))) {
|
|
2037
2930
|
return false;
|
|
2038
2931
|
}
|
|
2932
|
+
const result = await sessionService.setSessionModelConfig(
|
|
2933
|
+
route.sessionKey.trim(),
|
|
2934
|
+
{ fastMode: true },
|
|
2935
|
+
);
|
|
2936
|
+
return !!(result && result.status === "accepted");
|
|
2039
2937
|
},
|
|
2040
2938
|
onSessionActivated(route) {
|
|
2041
2939
|
if (!route || !route.sessionChanged) {
|
|
@@ -2052,15 +2950,70 @@ function createRelay(opts) {
|
|
|
2052
2950
|
});
|
|
2053
2951
|
}
|
|
2054
2952
|
|
|
2953
|
+
async function handleBufferedEvenAiHttpRequest(envelope) {
|
|
2954
|
+
if (!evenAiEndpoint || typeof evenAiEndpoint.handleRequest !== "function") {
|
|
2955
|
+
return {
|
|
2956
|
+
statusCode: 404,
|
|
2957
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
2958
|
+
body: Buffer.from("not found"),
|
|
2959
|
+
};
|
|
2960
|
+
}
|
|
2961
|
+
const req = createBufferedHttpRequest(envelope);
|
|
2962
|
+
const res = createBufferedHttpResponse(opts.evenAiMaxResponseBytes || 262_144);
|
|
2963
|
+
const requestId =
|
|
2964
|
+
envelope && typeof envelope.requestId === "string" ? envelope.requestId : null;
|
|
2965
|
+
if (requestId) {
|
|
2966
|
+
pendingBufferedEvenAiResponses.set(requestId, { req, res });
|
|
2967
|
+
}
|
|
2968
|
+
try {
|
|
2969
|
+
await Promise.resolve(evenAiEndpoint.handleRequest(req, res));
|
|
2970
|
+
if (!res.writableEnded) {
|
|
2971
|
+
res.statusCode = 404;
|
|
2972
|
+
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
2973
|
+
res.end("not found");
|
|
2974
|
+
}
|
|
2975
|
+
return res.toResult();
|
|
2976
|
+
} finally {
|
|
2977
|
+
if (requestId) {
|
|
2978
|
+
pendingBufferedEvenAiResponses.delete(requestId);
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
function cancelBufferedEvenAiHttpRequest(envelope) {
|
|
2984
|
+
const requestId =
|
|
2985
|
+
envelope && typeof envelope.requestId === "string" ? envelope.requestId : null;
|
|
2986
|
+
if (!requestId) {
|
|
2987
|
+
return false;
|
|
2988
|
+
}
|
|
2989
|
+
const pending = pendingBufferedEvenAiResponses.get(requestId);
|
|
2990
|
+
if (!pending) {
|
|
2991
|
+
return false;
|
|
2992
|
+
}
|
|
2993
|
+
pending.res.emit("close");
|
|
2994
|
+
pending.req.emit("close");
|
|
2995
|
+
return true;
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2055
2998
|
// --- Public API ---
|
|
2056
2999
|
|
|
2057
|
-
|
|
3000
|
+
relayApi = {
|
|
3001
|
+
/**
|
|
3002
|
+
* Emit a glasses-UI surface-lifecycle event on the permanent
|
|
3003
|
+
* `glasses.lifecycle` debug category (nav reconcile + cron pause/resume/
|
|
3004
|
+
* tick). Recorded only when the category is enabled via debug-set. Wired
|
|
3005
|
+
* through the relay-service facade into the glasses-ui tool handler + cron
|
|
3006
|
+
* engine. See docs/superpowers/findings/2026-05-30-glasses-ui-phase4-hardware.md.
|
|
3007
|
+
*/
|
|
3008
|
+
emitGlassesUiLifecycle(event, severity, data) {
|
|
3009
|
+
emitDebug("glasses.lifecycle", event, severity, {}, () => data || {});
|
|
3010
|
+
},
|
|
2058
3011
|
/**
|
|
2059
3012
|
* Start the upstream OpenClaw connection.
|
|
2060
3013
|
* The downstream server is already listening from construction.
|
|
2061
|
-
|
|
3014
|
+
*/
|
|
2062
3015
|
start() {
|
|
2063
|
-
|
|
3016
|
+
const startGateway = () => Promise.resolve(gatewayBridge.start()).then(() => {
|
|
2064
3017
|
prefetchSonioxModels("relay_start").catch((err) => {
|
|
2065
3018
|
logger.warn(`[relay] Soniox models prefetch failed: ${err.message}`);
|
|
2066
3019
|
});
|
|
@@ -2068,6 +3021,10 @@ function createRelay(opts) {
|
|
|
2068
3021
|
return upstreamRuntime.start();
|
|
2069
3022
|
}
|
|
2070
3023
|
});
|
|
3024
|
+
if (server && typeof server.start === "function") {
|
|
3025
|
+
return Promise.resolve(server.start()).then(startGateway);
|
|
3026
|
+
}
|
|
3027
|
+
return startGateway();
|
|
2071
3028
|
},
|
|
2072
3029
|
|
|
2073
3030
|
/**
|
|
@@ -2086,10 +3043,12 @@ function createRelay(opts) {
|
|
|
2086
3043
|
if (upstreamRuntime) {
|
|
2087
3044
|
upstreamRuntime.stop();
|
|
2088
3045
|
}
|
|
3046
|
+
relayHealth.stop();
|
|
2089
3047
|
gatewayBridge.stop();
|
|
2090
|
-
return Promise.
|
|
2091
|
-
|
|
2092
|
-
|
|
3048
|
+
return Promise.all([
|
|
3049
|
+
sessionService.flushFirstSentUserMessageCache(),
|
|
3050
|
+
Promise.resolve(server.close()),
|
|
3051
|
+
]).then(() => undefined);
|
|
2093
3052
|
},
|
|
2094
3053
|
|
|
2095
3054
|
handleEvenAiHttpRequest(req, res) {
|
|
@@ -2099,11 +3058,37 @@ function createRelay(opts) {
|
|
|
2099
3058
|
return Promise.resolve(evenAiEndpoint.handleRequest(req, res));
|
|
2100
3059
|
},
|
|
2101
3060
|
|
|
3061
|
+
handleBufferedEvenAiHttpRequest,
|
|
3062
|
+
|
|
2102
3063
|
/** The downstream server instance. */
|
|
2103
3064
|
get server() {
|
|
2104
3065
|
return server;
|
|
2105
3066
|
},
|
|
2106
3067
|
|
|
3068
|
+
get workerReadyForTest() {
|
|
3069
|
+
return server && server.readyPromise ? server.readyPromise : Promise.resolve();
|
|
3070
|
+
},
|
|
3071
|
+
|
|
3072
|
+
get debugStoreForTest() {
|
|
3073
|
+
return debugStore;
|
|
3074
|
+
},
|
|
3075
|
+
|
|
3076
|
+
get liveUiTraceLogEnabledForTest() {
|
|
3077
|
+
return liveUiTraceLogEnabled;
|
|
3078
|
+
},
|
|
3079
|
+
__onTraceLogSetForTest(clientId, request) {
|
|
3080
|
+
return applyTraceLogSet(clientId, request);
|
|
3081
|
+
},
|
|
3082
|
+
__onDebugSetForTest(clientId, request) {
|
|
3083
|
+
return applyDebugSet(clientId, request);
|
|
3084
|
+
},
|
|
3085
|
+
|
|
3086
|
+
get operationRegistryForTest() {
|
|
3087
|
+
return relayOperationRegistry;
|
|
3088
|
+
},
|
|
3089
|
+
|
|
3090
|
+
relayHealth,
|
|
3091
|
+
|
|
2107
3092
|
get httpServer() {
|
|
2108
3093
|
return sharedHttpServer;
|
|
2109
3094
|
},
|
|
@@ -2111,7 +3096,90 @@ function createRelay(opts) {
|
|
|
2111
3096
|
getEvenAiSettingsSnapshot() {
|
|
2112
3097
|
return evenAiSettingsStore.getSnapshot();
|
|
2113
3098
|
},
|
|
3099
|
+
|
|
3100
|
+
getSessionTitle(sessionKey) {
|
|
3101
|
+
return sessionService.getSessionTitle(sessionKey);
|
|
3102
|
+
},
|
|
3103
|
+
|
|
3104
|
+
hasRecordedUserMessage(sessionKey) {
|
|
3105
|
+
return sessionService.hasRecordedFirstUserMessage(sessionKey);
|
|
3106
|
+
},
|
|
3107
|
+
|
|
3108
|
+
isNeuralSessionNamesEnabled(sessionKey) {
|
|
3109
|
+
return sessionService.isNeuralSessionNamesEnabled(sessionKey);
|
|
3110
|
+
},
|
|
3111
|
+
|
|
3112
|
+
isSessionUserLocked(sessionKey) {
|
|
3113
|
+
return sessionService.isSessionUserLocked(sessionKey);
|
|
3114
|
+
},
|
|
3115
|
+
|
|
3116
|
+
peekSessionKey() {
|
|
3117
|
+
return sessionService.peekSessionKey();
|
|
3118
|
+
},
|
|
3119
|
+
|
|
3120
|
+
/**
|
|
3121
|
+
* Test/shutdown hook: resolves once the async first-user-message cache
|
|
3122
|
+
* write has fully drained (no write in flight, no dirty mark pending) so
|
|
3123
|
+
* the on-disk file reflects the latest in-memory map.
|
|
3124
|
+
*/
|
|
3125
|
+
flushFirstSentUserMessageCache() {
|
|
3126
|
+
return sessionService.flushFirstSentUserMessageCache();
|
|
3127
|
+
},
|
|
3128
|
+
|
|
3129
|
+
recordNeuralSessionNamesEnabled(sessionKey, enabled) {
|
|
3130
|
+
sessionService.recordNeuralSessionNamesEnabled(sessionKey, enabled);
|
|
3131
|
+
},
|
|
3132
|
+
|
|
3133
|
+
setSessionTitle(sessionKey, title, opts) {
|
|
3134
|
+
const result = sessionService.setSessionTitle(sessionKey, title, opts);
|
|
3135
|
+
if (result && result.ok) {
|
|
3136
|
+
broadcastSessions();
|
|
3137
|
+
}
|
|
3138
|
+
return result;
|
|
3139
|
+
},
|
|
3140
|
+
|
|
3141
|
+
/**
|
|
3142
|
+
* Test-only: direct access to dispatchOcuClawUserSend so integration
|
|
3143
|
+
* tests can drive per-turn signal plumbing without a live downstream
|
|
3144
|
+
* WebSocket connection.
|
|
3145
|
+
*/
|
|
3146
|
+
_dispatchOcuClawUserSend(params) {
|
|
3147
|
+
return dispatchOcuClawUserSend(params || {});
|
|
3148
|
+
},
|
|
3149
|
+
|
|
3150
|
+
sendGlassesUiRender(params) {
|
|
3151
|
+
sendGlassesUiRender(params);
|
|
3152
|
+
},
|
|
3153
|
+
|
|
3154
|
+
sendGlassesUiSurfaceUpdate(params) {
|
|
3155
|
+
sendGlassesUiSurfaceUpdate(params);
|
|
3156
|
+
},
|
|
3157
|
+
|
|
3158
|
+
onGlassesUiResult(handler) {
|
|
3159
|
+
return onGlassesUiResult(handler);
|
|
3160
|
+
},
|
|
3161
|
+
|
|
3162
|
+
onGlassesUiNavEvent(handler) {
|
|
3163
|
+
return onGlassesUiNavEvent(handler);
|
|
3164
|
+
},
|
|
3165
|
+
|
|
3166
|
+
sendDeviceInfoRequest(params) {
|
|
3167
|
+
sendDeviceInfoRequest(params);
|
|
3168
|
+
},
|
|
3169
|
+
|
|
3170
|
+
onDeviceInfoResponse(handler) {
|
|
3171
|
+
return onDeviceInfoResponse(handler);
|
|
3172
|
+
},
|
|
3173
|
+
|
|
3174
|
+
hasConnectedAppClient() {
|
|
3175
|
+
return server ? server.getConnectedAppCount() > 0 : false;
|
|
3176
|
+
},
|
|
3177
|
+
|
|
3178
|
+
onAppClientDisconnect(handler) {
|
|
3179
|
+
return onAppClientDisconnect(handler);
|
|
3180
|
+
},
|
|
2114
3181
|
};
|
|
3182
|
+
return relayApi;
|
|
2115
3183
|
}
|
|
2116
3184
|
|
|
2117
3185
|
const createRelayCore = createRelay;
|