ocuclaw 1.3.3 → 1.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -1
- package/dist/config/runtime-config-session-title-model.test.js +0 -3
- package/dist/config/runtime-config.js +22 -33
- package/dist/domain/activity-status-adapter.js +0 -7
- package/dist/domain/activity-status-arbiter.js +3 -27
- package/dist/domain/activity-status-labels.js +8 -38
- package/dist/domain/code-span-regions.js +4 -24
- package/dist/domain/constant-time-equal.js +9 -0
- package/dist/domain/constant-time-equal.test.js +28 -0
- package/dist/domain/conversation-state.js +27 -138
- package/dist/domain/debug-bundle-cache.js +52 -0
- package/dist/domain/debug-bundle-format.js +60 -0
- package/dist/domain/debug-bundle-preview.js +123 -0
- package/dist/domain/debug-bundle-redaction.js +182 -0
- package/dist/domain/debug-bundle-save.js +11 -0
- package/dist/domain/debug-bundle-zip.js +15 -0
- package/dist/domain/debug-bundle.js +97 -0
- package/dist/domain/debug-store.js +6 -17
- package/dist/domain/debug-upload-preset.js +27 -0
- package/dist/domain/glasses-display-system-prompt.js +0 -5
- package/dist/domain/glasses-display-system-prompt.test.js +1 -1
- package/dist/domain/glasses-ui-content-summary.js +0 -6
- package/dist/domain/glasses-ui-system-prompt.test.js +1 -2
- package/dist/domain/message-emoji-allowlist.js +0 -7
- package/dist/domain/message-emoji-filter.js +3 -9
- package/dist/domain/neural-emoji-reactor-tag-config.js +3 -3
- package/dist/domain/prompt-channel-fragments.js +1 -10
- package/dist/domain/tagged-span-parser.js +3 -26
- package/dist/domain/tagged-span-strip.js +0 -7
- package/dist/even-ai/even-ai-endpoint.js +77 -24
- package/dist/even-ai/even-ai-run-waiter.js +0 -1
- package/dist/even-ai/even-ai-settings-store.js +11 -0
- package/dist/gateway/gateway-bridge.js +8 -9
- package/dist/gateway/gateway-timing-ledger.js +8 -6
- package/dist/gateway/openclaw-client.js +97 -297
- package/dist/gateway/sanitize-connect-reason.js +10 -0
- package/dist/gateway/sanitize-connect-reason.test.js +34 -0
- package/dist/index.js +3 -3
- package/dist/runtime/channel-two-hook.js +1 -6
- package/dist/runtime/container-env.js +1 -5
- package/dist/runtime/debug-bundle-handler.js +159 -0
- package/dist/runtime/display-toggle-states.js +6 -17
- package/dist/runtime/downstream-handler.js +682 -508
- package/dist/runtime/glasses-backpressure-latch.js +2 -24
- package/dist/runtime/ocuclaw-settings-store.js +10 -1
- package/dist/runtime/openclaw-host-version.js +5 -0
- package/dist/runtime/plugin-version-service.js +13 -6
- package/dist/runtime/provider-usage-select.js +0 -6
- package/dist/runtime/register-session-title-distiller.js +14 -16
- package/dist/runtime/relay-core.js +601 -290
- package/dist/runtime/relay-service.js +19 -47
- package/dist/runtime/relay-worker-approval-replay-cache.js +1 -1
- package/dist/runtime/relay-worker-entry.js +1 -2
- package/dist/runtime/relay-worker-health.js +2 -10
- package/dist/runtime/relay-worker-protocol.js +6 -1
- package/dist/runtime/relay-worker-supervisor.js +103 -41
- package/dist/runtime/relay-worker-transport.js +150 -17
- package/dist/runtime/session-context-service.js +5 -45
- package/dist/runtime/session-service.js +157 -175
- package/dist/runtime/session-title-distiller-budget.js +1 -5
- package/dist/runtime/session-title-distiller-helpers.js +14 -24
- package/dist/runtime/session-title-distiller.js +109 -122
- package/dist/runtime/session-title-record.js +0 -6
- package/dist/runtime/stable-prompt-snapshot.js +3 -14
- package/dist/runtime/upstream-runtime.js +600 -103
- package/dist/tools/device-info-tool.js +4 -21
- package/dist/tools/glasses-ui-cron.js +22 -77
- package/dist/tools/glasses-ui-descriptors.js +4 -33
- package/dist/tools/glasses-ui-limits.js +0 -13
- package/dist/tools/glasses-ui-paint-floor.js +5 -39
- package/dist/tools/glasses-ui-recipes.js +92 -101
- package/dist/tools/glasses-ui-surfaces.js +31 -163
- package/dist/tools/glasses-ui-template.js +7 -22
- package/dist/tools/glasses-ui-tool-description.test.js +2 -2
- package/dist/tools/glasses-ui-tool.js +87 -451
- package/dist/tools/glasses-ui-voicemail.js +6 -63
- package/dist/tools/glasses-ui-wake.js +9 -76
- package/dist/tools/session-title-tool.js +2 -7
- package/dist/tools/session-title-tool.test.js +1 -1
- package/dist/version.js +3 -2
- package/openclaw.plugin.json +60 -13
- package/package.json +3 -2
- package/dist/runtime/protocol-adapter.js +0 -387
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import * as crypto from "node:crypto";
|
|
4
|
+
import * as os from "node:os";
|
|
3
5
|
import { EventEmitter } from "node:events";
|
|
4
6
|
import { createPluginVersionService } from "./plugin-version-service.js";
|
|
5
7
|
import * as conversationStateModule from "../domain/conversation-state.js";
|
|
6
8
|
import { createDebugStore } from "../domain/debug-store.js";
|
|
9
|
+
import { startUploadCaptureArming, UPLOAD_CAPTURE_PRESET } from "../domain/debug-upload-preset.js";
|
|
7
10
|
import { summarizeGlassesUiContent } from "../domain/glasses-ui-content-summary.js";
|
|
8
11
|
import { composeReadabilitySystemPrompt } from "../domain/readability-system-prompt.js";
|
|
9
12
|
import { composeNeuralEmojiReactorSystemPrompt } from "../domain/neural-emoji-reactor-system-prompt.js";
|
|
@@ -15,12 +18,21 @@ import { createActivityStatusAdapter } from "../domain/activity-status-adapter.j
|
|
|
15
18
|
import { createEvenAiEndpoint } from "../even-ai/even-ai-endpoint.js";
|
|
16
19
|
import { createEvenAiRouter } from "../even-ai/even-ai-router.js";
|
|
17
20
|
import { createEvenAiRunWaiter } from "../even-ai/even-ai-run-waiter.js";
|
|
18
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
createEvenAiSettingsStore,
|
|
23
|
+
normalizeEvenAiDefaultAgent,
|
|
24
|
+
} from "../even-ai/even-ai-settings-store.js";
|
|
19
25
|
import { createPluginOpenclawClient } from "../gateway/openclaw-client.js";
|
|
20
26
|
import { createPluginRpcGatewayBridge } from "../gateway/gateway-bridge.js";
|
|
21
27
|
import { createAgentTurnTracker } from "../tools/glasses-ui-wake.js";
|
|
22
28
|
import { createDownstreamHandler } from "./downstream-handler.js";
|
|
23
|
-
import {
|
|
29
|
+
import { handleDebugBundleRequest, handleDebugBundleSave, handleDebugBundleFetch } from "./debug-bundle-handler.js";
|
|
30
|
+
import { createBundleCache } from "../domain/debug-bundle-cache.js";
|
|
31
|
+
import { saveBundleToDisk } from "../domain/debug-bundle-save.js";
|
|
32
|
+
import {
|
|
33
|
+
createOcuClawSettingsStore,
|
|
34
|
+
normalizeOcuClawDefaultAgent,
|
|
35
|
+
} from "./ocuclaw-settings-store.js";
|
|
24
36
|
import { createRelayHealthMonitor } from "./relay-health-monitor.js";
|
|
25
37
|
import { createGlassesBackpressureLatch } from "./glasses-backpressure-latch.js";
|
|
26
38
|
import { createRelayOperationRegistry } from "./relay-operation-registry.js";
|
|
@@ -37,10 +49,12 @@ export function sanitizeGlassesMarker(v) { return GLASSES_UI_MARKERS.has(v) ? v
|
|
|
37
49
|
const SONIOX_TEMP_KEY_URL = "https://api.soniox.com/v1/auth/temporary-api-key";
|
|
38
50
|
const SONIOX_MODELS_URL = "https://api.soniox.com/v1/models";
|
|
39
51
|
const DEFAULT_SONIOX_TEMP_KEY_EXPIRES_IN_SECONDS = 3600;
|
|
40
|
-
|
|
41
|
-
// fetch. 8 s is a conservative cold-path ceiling; tests inject a tiny value
|
|
42
|
-
// via opts.sonioxTemporaryKeyMintTimeoutMs to make assertions fast.
|
|
52
|
+
|
|
43
53
|
const DEFAULT_SONIOX_TEMP_KEY_MINT_TIMEOUT_MS = 8000;
|
|
54
|
+
const CARTESIA_ACCESS_TOKEN_URL = "https://api.cartesia.ai/access-token";
|
|
55
|
+
const CARTESIA_VERSION = "2026-03-01";
|
|
56
|
+
const DEFAULT_CARTESIA_ACCESS_TOKEN_EXPIRES_IN_SECONDS = 3600;
|
|
57
|
+
const DEFAULT_CARTESIA_ACCESS_TOKEN_MINT_TIMEOUT_MS = 8000;
|
|
44
58
|
const EVEN_AI_NAMESPACE_PREFIX = "ocuclaw:even-ai";
|
|
45
59
|
const EVEN_AI_NAMESPACE_PREFIX_WITH_DELIMITER = "ocuclaw:even-ai:";
|
|
46
60
|
const LISTEN_INTERCEPT_RECOVERY_ERROR = "Voice interrupted; retry";
|
|
@@ -179,7 +193,7 @@ function normalizeSonioxTemporaryKeyErrorCode(err) {
|
|
|
179
193
|
: "";
|
|
180
194
|
const lowered = message.toLowerCase();
|
|
181
195
|
if (!message) return "soniox_temp_key_request_failed";
|
|
182
|
-
|
|
196
|
+
|
|
183
197
|
if (err && err.name === "AbortError") {
|
|
184
198
|
return "soniox_temp_key_mint_timeout";
|
|
185
199
|
}
|
|
@@ -261,9 +275,7 @@ function createBufferedHttpResponse(maxResponseBytes) {
|
|
|
261
275
|
const limit = Number.isFinite(maxResponseBytes) && maxResponseBytes > 0
|
|
262
276
|
? Math.floor(maxResponseBytes)
|
|
263
277
|
: 262_144;
|
|
264
|
-
|
|
265
|
-
// res.once('close', ...) for client-disconnect detection. Worker-mode
|
|
266
|
-
// relays actual client closes through an http.cancel worker message.
|
|
278
|
+
|
|
267
279
|
const res = new EventEmitter();
|
|
268
280
|
res.statusCode = 200;
|
|
269
281
|
res.writableEnded = false;
|
|
@@ -301,29 +313,18 @@ function createBufferedHttpResponse(maxResponseBytes) {
|
|
|
301
313
|
return res;
|
|
302
314
|
}
|
|
303
315
|
|
|
304
|
-
// --- Factory ---
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Create the relay orchestrator.
|
|
308
|
-
*
|
|
309
|
-
* Wires the upstream gateway bridge to downstream clients via the
|
|
310
|
-
* conversation-state module, downstream handler, and downstream server.
|
|
311
|
-
* This is the only module that knows about both sides.
|
|
312
|
-
*
|
|
313
|
-
* @param {object} opts
|
|
314
|
-
* @param {number} opts.port - WebSocket server port
|
|
315
|
-
* @param {string} opts.host - WebSocket server bind address
|
|
316
|
-
* @param {string} opts.token - Authentication token for downstream clients
|
|
317
|
-
* @param {object} [opts.gatewayBridge] - Override bridge for testing/integration
|
|
318
|
-
* @param {object} [opts.openclawClient] - Override for testing (default: plugin gateway client)
|
|
319
|
-
* @param {object} [opts.conversationState] - Override for testing (default: require singleton)
|
|
320
|
-
* @param {object} [opts.logger] - Structured logger for shared runtime logs
|
|
321
|
-
* @param {string|null} [opts.consoleLogPath] - Optional shim-only browser console sink path
|
|
322
|
-
* @returns {object} Relay instance with start(), stop(), server
|
|
323
|
-
*/
|
|
324
316
|
function createRelay(opts) {
|
|
325
317
|
const logger = normalizeLogger(opts.logger);
|
|
326
318
|
const externalDebugToolsEnabled = opts.externalDebugToolsEnabled !== false;
|
|
319
|
+
|
|
320
|
+
const allowDebugUpload = opts.allowDebugUpload === true;
|
|
321
|
+
|
|
322
|
+
const debugUploadMaxZipBytes =
|
|
323
|
+
Number.isFinite(opts.debugUploadMaxZipBytes) && opts.debugUploadMaxZipBytes > 0
|
|
324
|
+
? Math.floor(opts.debugUploadMaxZipBytes)
|
|
325
|
+
: 4_000_000;
|
|
326
|
+
|
|
327
|
+
const debugBundleIdSalt = crypto.randomBytes(16).toString("hex");
|
|
327
328
|
const openclawClient =
|
|
328
329
|
opts.openclawClient ||
|
|
329
330
|
(opts.gatewayBridge
|
|
@@ -347,36 +348,23 @@ function createRelay(opts) {
|
|
|
347
348
|
const activityStatusAdapter = createActivityStatusAdapter(
|
|
348
349
|
opts.activityStatusAdapter,
|
|
349
350
|
);
|
|
350
|
-
|
|
351
|
-
// marked busy on every dispatched send (voice/user/wake), refreshed by the
|
|
352
|
-
// gateway activity stream, idled on end-phase activity, decay-bounded
|
|
353
|
-
// (fail open). The glasses-ui wake controller consults it so a wake never
|
|
354
|
-
// races a genuine turn (voice absorbs wake, §2.6c).
|
|
351
|
+
|
|
355
352
|
const agentTurnTracker = createAgentTurnTracker();
|
|
356
353
|
const sharedHttpServer = opts.httpServer || null;
|
|
357
354
|
|
|
358
|
-
// --- Cached state ---
|
|
359
|
-
|
|
360
|
-
/** @type {string|null} Last formatted pages JSON string. */
|
|
361
355
|
let cachedPages = null;
|
|
362
|
-
|
|
356
|
+
|
|
363
357
|
let pagesRevision = 0;
|
|
364
358
|
|
|
365
|
-
/** @type {string|null} Last formatted status JSON string. */
|
|
366
359
|
let cachedStatus = null;
|
|
367
|
-
|
|
360
|
+
|
|
368
361
|
let statusRevision = 0;
|
|
369
|
-
|
|
362
|
+
|
|
370
363
|
let currentSessionModelConfigSnapshot = null;
|
|
371
364
|
|
|
372
|
-
/** Relay-local deterministic simulate-stream run sequence counter. */
|
|
373
365
|
let simulateStreamRunSeq = 0;
|
|
374
|
-
/** Active timers for relay-local deterministic simulate-stream runs. */
|
|
375
|
-
// timer -> sessionKey, so new-chat//reset/new-session can cancel ONLY the
|
|
376
|
-
// affected session's pending injections (re-land of a8a29032, session-scoped).
|
|
377
|
-
const simulateStreamTimers = new Map();
|
|
378
366
|
|
|
379
|
-
|
|
367
|
+
const simulateStreamTimers = new Map();
|
|
380
368
|
|
|
381
369
|
const debugCategories = Array.isArray(opts.debugCategories)
|
|
382
370
|
? opts.debugCategories
|
|
@@ -385,21 +373,10 @@ function createRelay(opts) {
|
|
|
385
373
|
.filter(([, enabled]) => enabled)
|
|
386
374
|
.map(([category]) => category)
|
|
387
375
|
: opts.debugCategories;
|
|
388
|
-
|
|
389
|
-
// liveui log tee all read from this one source so store records and `[liveui]`
|
|
390
|
-
// log lines share an identical ts (downstream reconcilers dedupe on it).
|
|
376
|
+
|
|
391
377
|
const debugNow =
|
|
392
378
|
typeof opts.debugNow === "function" ? opts.debugNow : () => Date.now();
|
|
393
379
|
|
|
394
|
-
// --- Durable debug-store arm (survives relay/gateway restarts) ---
|
|
395
|
-
// The capture arm (enabled categories + TTLs) lives only in the in-memory
|
|
396
|
-
// debug-store, which a restart rebuilds empty. We persist it to debug-arm.json
|
|
397
|
-
// (via persistDebugArm) and rehydrate it here at construction — read-once,
|
|
398
|
-
// mirroring liveUiTraceFlagPath below — so a restart no longer silently drops
|
|
399
|
-
// capture. This path covers process RESTART only: a pure WebUI *reload* does NOT
|
|
400
|
-
// restart the relay and already preserves + re-advertises the arm via
|
|
401
|
-
// relay-worker-transport.ts:327-328 (cache.debugConfig re-broadcast to app
|
|
402
|
-
// clients) — do not add reconnect machinery here.
|
|
403
380
|
const debugArmStatePath =
|
|
404
381
|
typeof opts.stateDir === "string" && opts.stateDir
|
|
405
382
|
? path.join(opts.stateDir, "debug-arm.json")
|
|
@@ -418,7 +395,6 @@ function createRelay(opts) {
|
|
|
418
395
|
const debugStore = createDebugStore({
|
|
419
396
|
categories: debugCategories,
|
|
420
397
|
capacity: opts.debugCapacity,
|
|
421
|
-
payloadMaxBytes: opts.debugPayloadMaxBytes,
|
|
422
398
|
defaultTtlMs: opts.debugDefaultTtlMs,
|
|
423
399
|
maxTtlMs: opts.debugMaxTtlMs,
|
|
424
400
|
dumpDefaultLimit: opts.debugDumpDefaultLimit,
|
|
@@ -428,10 +404,14 @@ function createRelay(opts) {
|
|
|
428
404
|
initialEnabled: initialDebugArm,
|
|
429
405
|
});
|
|
430
406
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
407
|
+
const bundleCache = createBundleCache({ maxEntries: 4, ttlMs: 5 * 60_000, now: () => Date.now() });
|
|
408
|
+
let bundleCacheSweepTimer = null;
|
|
409
|
+
|
|
410
|
+
function resolveSaveDir() {
|
|
411
|
+
const c = opts.debugBundleSaveDir;
|
|
412
|
+
return (typeof c === "string" && c.trim()) ? c : path.join(os.homedir(), ".openclaw", "ocuclaw-debug-bundles");
|
|
413
|
+
}
|
|
414
|
+
|
|
435
415
|
const liveUiTraceFlagPath =
|
|
436
416
|
typeof opts.stateDir === "string" && opts.stateDir
|
|
437
417
|
? path.join(opts.stateDir, "liveui-trace.json")
|
|
@@ -446,13 +426,11 @@ function createRelay(opts) {
|
|
|
446
426
|
}
|
|
447
427
|
}
|
|
448
428
|
|
|
449
|
-
// --- Console log file ---
|
|
450
|
-
|
|
451
429
|
const consoleLogPath =
|
|
452
430
|
typeof opts.consoleLogPath === "string" && opts.consoleLogPath.trim()
|
|
453
431
|
? opts.consoleLogPath
|
|
454
432
|
: null;
|
|
455
|
-
|
|
433
|
+
|
|
456
434
|
if (consoleLogPath) {
|
|
457
435
|
try {
|
|
458
436
|
fs.writeFileSync(consoleLogPath, "");
|
|
@@ -461,13 +439,6 @@ function createRelay(opts) {
|
|
|
461
439
|
const CONSOLE_LOG_MAX_LINES = 500;
|
|
462
440
|
const CONSOLE_LOG_TRIM_TO = 250;
|
|
463
441
|
|
|
464
|
-
/**
|
|
465
|
-
* Append a browser console message to the log file.
|
|
466
|
-
* Trims the file when it exceeds CONSOLE_LOG_MAX_LINES.
|
|
467
|
-
*
|
|
468
|
-
* @param {string} level - "log", "warn", or "error"
|
|
469
|
-
* @param {string} message - Console message text
|
|
470
|
-
*/
|
|
471
442
|
function writeConsoleLog(level, message) {
|
|
472
443
|
if (!consoleLogPath) {
|
|
473
444
|
logger.debug(`[browser:${level}] ${message}`);
|
|
@@ -477,7 +448,7 @@ function createRelay(opts) {
|
|
|
477
448
|
const line = `[${timestamp}] [${level}] ${message}\n`;
|
|
478
449
|
try {
|
|
479
450
|
fs.appendFileSync(consoleLogPath, line);
|
|
480
|
-
|
|
451
|
+
|
|
481
452
|
const content = fs.readFileSync(consoleLogPath, "utf8");
|
|
482
453
|
const lines = content.split("\n");
|
|
483
454
|
if (lines.length > CONSOLE_LOG_MAX_LINES) {
|
|
@@ -489,16 +460,6 @@ function createRelay(opts) {
|
|
|
489
460
|
}
|
|
490
461
|
}
|
|
491
462
|
|
|
492
|
-
/**
|
|
493
|
-
* Emit a structured debug event when a category is enabled.
|
|
494
|
-
* This keeps the disabled path cheap by avoiding payload construction.
|
|
495
|
-
*
|
|
496
|
-
* @param {string} cat
|
|
497
|
-
* @param {string} event
|
|
498
|
-
* @param {"debug"|"info"|"warn"|"error"} severity
|
|
499
|
-
* @param {object} context
|
|
500
|
-
* @param {() => object} buildData
|
|
501
|
-
*/
|
|
502
463
|
function emitDebug(cat, event, severity, context, buildData, options) {
|
|
503
464
|
const force = !!(options && options.force === true);
|
|
504
465
|
if (!force && !debugStore.isEnabled(cat) && !(liveUiTraceLogEnabled && (cat === "glasses.lifecycle" || cat === "openclaw.message"))) {
|
|
@@ -523,8 +484,6 @@ function createRelay(opts) {
|
|
|
523
484
|
|
|
524
485
|
debugStore.emit(payload, { force });
|
|
525
486
|
|
|
526
|
-
// Durable openclaw-side trace tee (gated by the persistent flag, NOT the
|
|
527
|
-
// store category enable). Must never throw into the emit path.
|
|
528
487
|
if (liveUiTraceLogEnabled && (cat === "glasses.lifecycle" || cat === "openclaw.message")) {
|
|
529
488
|
try {
|
|
530
489
|
const surfaceId =
|
|
@@ -552,7 +511,7 @@ function createRelay(opts) {
|
|
|
552
511
|
}),
|
|
553
512
|
);
|
|
554
513
|
} catch {
|
|
555
|
-
|
|
514
|
+
|
|
556
515
|
}
|
|
557
516
|
}
|
|
558
517
|
}
|
|
@@ -620,12 +579,26 @@ function createRelay(opts) {
|
|
|
620
579
|
)
|
|
621
580
|
? Math.max(1, Math.floor(opts.sonioxTemporaryKeyMintTimeoutMs))
|
|
622
581
|
: DEFAULT_SONIOX_TEMP_KEY_MINT_TIMEOUT_MS;
|
|
623
|
-
|
|
582
|
+
const configuredCartesiaApiKey =
|
|
583
|
+
opts.cartesiaApiKey !== undefined
|
|
584
|
+
? opts.cartesiaApiKey
|
|
585
|
+
: (opts.config && opts.config.cartesiaApiKey) || "";
|
|
586
|
+
const cartesiaAccessTokenExpiresInSeconds = Number.isFinite(
|
|
587
|
+
opts.cartesiaAccessTokenExpiresInSeconds,
|
|
588
|
+
)
|
|
589
|
+
? Math.max(30, Math.min(3600, Math.floor(opts.cartesiaAccessTokenExpiresInSeconds)))
|
|
590
|
+
: DEFAULT_CARTESIA_ACCESS_TOKEN_EXPIRES_IN_SECONDS;
|
|
591
|
+
const cartesiaAccessTokenMintTimeoutMs = Number.isFinite(
|
|
592
|
+
opts.cartesiaAccessTokenMintTimeoutMs,
|
|
593
|
+
)
|
|
594
|
+
? Math.max(1, Math.floor(opts.cartesiaAccessTokenMintTimeoutMs))
|
|
595
|
+
: DEFAULT_CARTESIA_ACCESS_TOKEN_MINT_TIMEOUT_MS;
|
|
596
|
+
|
|
624
597
|
let cachedSonioxModels = null;
|
|
625
598
|
let cachedSonioxModelsFetchedAt = 0;
|
|
626
599
|
let cachedSonioxModelsStale = true;
|
|
627
600
|
let sonioxModelsFetchStarted = false;
|
|
628
|
-
|
|
601
|
+
|
|
629
602
|
let inFlightSonioxModelsFetch = null;
|
|
630
603
|
|
|
631
604
|
function resolveFetchImpl() {
|
|
@@ -937,6 +910,138 @@ function createRelay(opts) {
|
|
|
937
910
|
}
|
|
938
911
|
}
|
|
939
912
|
|
|
913
|
+
function normalizeCartesiaAccessTokenResult(result, voiceSessionId, nowMs) {
|
|
914
|
+
const accessToken =
|
|
915
|
+
pickTrimmedString(result && (result.accessToken || result.token)) || "";
|
|
916
|
+
if (!accessToken) {
|
|
917
|
+
throw new Error("Cartesia access-token response missing token");
|
|
918
|
+
}
|
|
919
|
+
const expiresInSeconds = Number.isFinite(result && result.expiresInSeconds)
|
|
920
|
+
? result.expiresInSeconds
|
|
921
|
+
: cartesiaAccessTokenExpiresInSeconds;
|
|
922
|
+
const expiresAtMs = Number.isFinite(result && result.expiresAtMs)
|
|
923
|
+
? Math.floor(result.expiresAtMs)
|
|
924
|
+
: Math.floor(nowMs + expiresInSeconds * 1000);
|
|
925
|
+
return { voiceSessionId, accessToken, expiresAtMs };
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
async function mintCartesiaAccessToken(clientId, request) {
|
|
929
|
+
const voiceSessionId = pickTrimmedString(request && request.voiceSessionId);
|
|
930
|
+
if (!voiceSessionId) {
|
|
931
|
+
throw new Error("voiceSessionId is required");
|
|
932
|
+
}
|
|
933
|
+
const sessionKey = pickTrimmedString(request && request.sessionKey) || null;
|
|
934
|
+
const nowMs = Date.now();
|
|
935
|
+
const resolvedSessionKey = sessionKey || sessionService.peekSessionKey() || undefined;
|
|
936
|
+
const emitIssued = (normalized, source) => {
|
|
937
|
+
logger.info(
|
|
938
|
+
`[relay] cartesia access token issued: clientId=${clientId} voiceSessionId=${voiceSessionId} source=${source} expiresAtMs=${normalized.expiresAtMs}`,
|
|
939
|
+
);
|
|
940
|
+
emitDebug(
|
|
941
|
+
"voice.timeline",
|
|
942
|
+
"cartesia_access_token_issued",
|
|
943
|
+
"info",
|
|
944
|
+
{ sessionKey: resolvedSessionKey },
|
|
945
|
+
() => ({ clientId, voiceSessionId, expiresAtMs: normalized.expiresAtMs, source }),
|
|
946
|
+
);
|
|
947
|
+
return normalized;
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
try {
|
|
951
|
+
if (typeof opts.createCartesiaAccessToken === "function") {
|
|
952
|
+
const overrideResult = await Promise.resolve(
|
|
953
|
+
opts.createCartesiaAccessToken({
|
|
954
|
+
voiceSessionId,
|
|
955
|
+
sessionKey,
|
|
956
|
+
expiresInSeconds: cartesiaAccessTokenExpiresInSeconds,
|
|
957
|
+
}),
|
|
958
|
+
);
|
|
959
|
+
return emitIssued(
|
|
960
|
+
normalizeCartesiaAccessTokenResult(overrideResult || {}, voiceSessionId, nowMs),
|
|
961
|
+
"override",
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
if (!configuredCartesiaApiKey) {
|
|
966
|
+
throw new Error("Cartesia API key is not configured");
|
|
967
|
+
}
|
|
968
|
+
const fetchImpl = resolveFetchImpl();
|
|
969
|
+
if (!fetchImpl) {
|
|
970
|
+
throw new Error("fetch is not available for Cartesia access-token minting");
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
const mintAbortController = new AbortController();
|
|
974
|
+
const mintTimeoutTimer = setTimeout(
|
|
975
|
+
() => mintAbortController.abort(),
|
|
976
|
+
cartesiaAccessTokenMintTimeoutMs,
|
|
977
|
+
);
|
|
978
|
+
let response;
|
|
979
|
+
try {
|
|
980
|
+
response = await fetchImpl(CARTESIA_ACCESS_TOKEN_URL, {
|
|
981
|
+
method: "POST",
|
|
982
|
+
headers: {
|
|
983
|
+
Authorization: `Bearer ${configuredCartesiaApiKey}`,
|
|
984
|
+
"Cartesia-Version": CARTESIA_VERSION,
|
|
985
|
+
"Content-Type": "application/json",
|
|
986
|
+
},
|
|
987
|
+
body: JSON.stringify({
|
|
988
|
+
grants: { stt: true },
|
|
989
|
+
expires_in: cartesiaAccessTokenExpiresInSeconds,
|
|
990
|
+
}),
|
|
991
|
+
signal: mintAbortController.signal,
|
|
992
|
+
});
|
|
993
|
+
} finally {
|
|
994
|
+
clearTimeout(mintTimeoutTimer);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
const rawText =
|
|
998
|
+
response && typeof response.text === "function" ? await response.text() : "";
|
|
999
|
+
let payload = {};
|
|
1000
|
+
if (rawText) {
|
|
1001
|
+
try {
|
|
1002
|
+
payload = JSON.parse(rawText);
|
|
1003
|
+
} catch (err) {
|
|
1004
|
+
throw new Error(
|
|
1005
|
+
`Cartesia access-token response was not valid JSON (${response.status})`,
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
if (!response.ok) {
|
|
1010
|
+
const errorDetail =
|
|
1011
|
+
pickTrimmedString(
|
|
1012
|
+
payload && payload.message,
|
|
1013
|
+
payload && payload.error,
|
|
1014
|
+
rawText,
|
|
1015
|
+
) || `HTTP ${response.status}`;
|
|
1016
|
+
throw new Error(
|
|
1017
|
+
`Cartesia access-token request failed (${response.status}): ${tailForLog(errorDetail)}`,
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
return emitIssued(
|
|
1022
|
+
normalizeCartesiaAccessTokenResult(
|
|
1023
|
+
{ token: payload && payload.token },
|
|
1024
|
+
voiceSessionId,
|
|
1025
|
+
nowMs,
|
|
1026
|
+
),
|
|
1027
|
+
"cartesia_api",
|
|
1028
|
+
);
|
|
1029
|
+
} catch (err) {
|
|
1030
|
+
const message = err && err.message ? err.message : "Cartesia access-token request failed";
|
|
1031
|
+
logger.warn(
|
|
1032
|
+
`[relay] cartesia access token failed: clientId=${clientId} voiceSessionId=${voiceSessionId} message=${tailForLog(message)}`,
|
|
1033
|
+
);
|
|
1034
|
+
emitDebug(
|
|
1035
|
+
"voice.timeline",
|
|
1036
|
+
"cartesia_access_token_failed",
|
|
1037
|
+
"warn",
|
|
1038
|
+
{ sessionKey: resolvedSessionKey },
|
|
1039
|
+
() => ({ clientId, voiceSessionId, message: tailForLog(message) }),
|
|
1040
|
+
);
|
|
1041
|
+
throw err;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
940
1045
|
let upstreamRuntime = null;
|
|
941
1046
|
const evenAiSettingsStore = createEvenAiSettingsStore({
|
|
942
1047
|
logger,
|
|
@@ -959,16 +1064,11 @@ function createRelay(opts) {
|
|
|
959
1064
|
stateDir: opts.stateDir,
|
|
960
1065
|
emitDebug,
|
|
961
1066
|
});
|
|
962
|
-
|
|
963
|
-
// cleared in stop(). Declared here so both can see it.
|
|
1067
|
+
|
|
964
1068
|
let stablePromptSweepTimer = null;
|
|
965
1069
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
// session's lifetime (see stable-prompt-snapshot). Mid-session toggles are
|
|
969
|
-
// bridged by the Channel-2 hook composer, NOT here. The glasses-UI pointer is
|
|
970
|
-
// always present (its disconnected gate moved to Channel 2); the emoji/pace
|
|
971
|
-
// blocks are included only when active at session start.
|
|
1070
|
+
let uploadCaptureArmingDisposer = null;
|
|
1071
|
+
|
|
972
1072
|
function computeStableChannelOne(startSignals) {
|
|
973
1073
|
const baseReadability = composeReadabilitySystemPrompt(
|
|
974
1074
|
ocuClawSettingsStore.getSnapshot().systemPrompt,
|
|
@@ -987,8 +1087,7 @@ function createRelay(opts) {
|
|
|
987
1087
|
|
|
988
1088
|
function stableSendOptions(resolvedSessionKey, sessionId, perTurnSignals) {
|
|
989
1089
|
const signals = perTurnSignals || {};
|
|
990
|
-
|
|
991
|
-
// session. The 3-state client signal maps to a boolean: only "active" counts.
|
|
1090
|
+
|
|
992
1091
|
const startEmoji = signals.neuralEmojiReactorState === "active";
|
|
993
1092
|
const startPace = signals.neuralPaceModulatorState === "active";
|
|
994
1093
|
const extraSystemPrompt = stablePromptSnapshots.getOrCreate(
|
|
@@ -996,10 +1095,7 @@ function createRelay(opts) {
|
|
|
996
1095
|
sessionId,
|
|
997
1096
|
() => computeStableChannelOne({ emoji: startEmoji, pace: startPace }),
|
|
998
1097
|
);
|
|
999
|
-
|
|
1000
|
-
// something is mutating Channel 1 mid-session (e.g. a toggle flipped the
|
|
1001
|
-
// start-state signals) — the case that used to reset the CLI session.
|
|
1002
|
-
// Surface it loudly; the served prompt stays the frozen snapshot.
|
|
1098
|
+
|
|
1003
1099
|
if (
|
|
1004
1100
|
stablePromptSnapshots.wouldChurn(
|
|
1005
1101
|
resolvedSessionKey,
|
|
@@ -1015,7 +1111,13 @@ function createRelay(opts) {
|
|
|
1015
1111
|
() => ({ sessionId: sessionId || null }),
|
|
1016
1112
|
);
|
|
1017
1113
|
}
|
|
1018
|
-
|
|
1114
|
+
const options = { extraSystemPrompt };
|
|
1115
|
+
|
|
1116
|
+
const agentId = sessionService.getSessionAgentId(resolvedSessionKey);
|
|
1117
|
+
if (typeof agentId === "string" && agentId.trim()) {
|
|
1118
|
+
options.agentId = agentId.trim();
|
|
1119
|
+
}
|
|
1120
|
+
return options;
|
|
1019
1121
|
}
|
|
1020
1122
|
|
|
1021
1123
|
function buildOcuClawSendDiagnostic(params = {}) {
|
|
@@ -1077,6 +1179,36 @@ function createRelay(opts) {
|
|
|
1077
1179
|
return userContent;
|
|
1078
1180
|
}
|
|
1079
1181
|
|
|
1182
|
+
function buildGatewayAttachment(attachment) {
|
|
1183
|
+
if (
|
|
1184
|
+
!attachment ||
|
|
1185
|
+
typeof attachment !== "object" ||
|
|
1186
|
+
typeof attachment.base64Data !== "string" ||
|
|
1187
|
+
!attachment.base64Data
|
|
1188
|
+
) {
|
|
1189
|
+
return null;
|
|
1190
|
+
}
|
|
1191
|
+
const normalizedAttachment = {
|
|
1192
|
+
type: attachment.kind || "image",
|
|
1193
|
+
mimeType: attachment.mimeType || "image/jpeg",
|
|
1194
|
+
fileName: attachment.name || "image.jpg",
|
|
1195
|
+
content: attachment.base64Data,
|
|
1196
|
+
};
|
|
1197
|
+
if (typeof attachment.source === "string" && attachment.source) {
|
|
1198
|
+
normalizedAttachment.source = attachment.source;
|
|
1199
|
+
}
|
|
1200
|
+
if (Number.isFinite(attachment.sizeBytes) && attachment.sizeBytes > 0) {
|
|
1201
|
+
normalizedAttachment.sizeBytes = Math.floor(attachment.sizeBytes);
|
|
1202
|
+
}
|
|
1203
|
+
if (Number.isFinite(attachment.widthPx) && attachment.widthPx > 0) {
|
|
1204
|
+
normalizedAttachment.widthPx = Math.floor(attachment.widthPx);
|
|
1205
|
+
}
|
|
1206
|
+
if (Number.isFinite(attachment.heightPx) && attachment.heightPx > 0) {
|
|
1207
|
+
normalizedAttachment.heightPx = Math.floor(attachment.heightPx);
|
|
1208
|
+
}
|
|
1209
|
+
return normalizedAttachment;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1080
1212
|
function buildOcuClawInitialSessionConfigPatch(settings) {
|
|
1081
1213
|
const patch = {};
|
|
1082
1214
|
if (settings && typeof settings.defaultModel === "string" && settings.defaultModel.trim()) {
|
|
@@ -1095,6 +1227,30 @@ function createRelay(opts) {
|
|
|
1095
1227
|
return Object.keys(patch).length > 0 ? patch : null;
|
|
1096
1228
|
}
|
|
1097
1229
|
|
|
1230
|
+
function seedSessionAgentDefault(sessionKey, defaultAgent) {
|
|
1231
|
+
if (
|
|
1232
|
+
!sessionKey ||
|
|
1233
|
+
!sessionService ||
|
|
1234
|
+
typeof sessionService.setSessionAgentId !== "function" ||
|
|
1235
|
+
typeof sessionService.getSessionAgentId !== "function"
|
|
1236
|
+
) {
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
const normalized =
|
|
1240
|
+
typeof defaultAgent === "string" ? defaultAgent.trim() : "";
|
|
1241
|
+
if (!normalized) {
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
if (
|
|
1245
|
+
typeof sessionService.hasExplicitSessionAgent === "function" &&
|
|
1246
|
+
sessionService.hasExplicitSessionAgent(sessionKey)
|
|
1247
|
+
) {
|
|
1248
|
+
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
sessionService.setSessionAgentId(sessionKey, normalized);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1098
1254
|
async function maybeSeedOcuClawSessionConfig(sessionKey) {
|
|
1099
1255
|
if (
|
|
1100
1256
|
!sessionKey ||
|
|
@@ -1106,6 +1262,7 @@ function createRelay(opts) {
|
|
|
1106
1262
|
}
|
|
1107
1263
|
|
|
1108
1264
|
const settings = ocuClawSettingsStore.getSnapshot();
|
|
1265
|
+
seedSessionAgentDefault(sessionKey, settings.defaultAgent);
|
|
1109
1266
|
const patch = buildOcuClawInitialSessionConfigPatch(settings);
|
|
1110
1267
|
if (!patch) {
|
|
1111
1268
|
sessionService.clearPendingInitialConfig(sessionKey);
|
|
@@ -1133,6 +1290,7 @@ function createRelay(opts) {
|
|
|
1133
1290
|
}
|
|
1134
1291
|
|
|
1135
1292
|
const settings = ocuClawSettingsStore.getSnapshot();
|
|
1293
|
+
seedSessionAgentDefault(sessionKey, settings.defaultAgent);
|
|
1136
1294
|
const patch = buildOcuClawInitialSessionConfigPatch(settings);
|
|
1137
1295
|
if (!patch) {
|
|
1138
1296
|
return null;
|
|
@@ -1165,6 +1323,17 @@ function createRelay(opts) {
|
|
|
1165
1323
|
getAgentName() {
|
|
1166
1324
|
return upstreamRuntime ? upstreamRuntime.getAgentName() : null;
|
|
1167
1325
|
},
|
|
1326
|
+
getAgentDisplayName(agentId) {
|
|
1327
|
+
return upstreamRuntime &&
|
|
1328
|
+
typeof upstreamRuntime.getAgentDisplayName === "function"
|
|
1329
|
+
? upstreamRuntime.getAgentDisplayName(agentId)
|
|
1330
|
+
: null;
|
|
1331
|
+
},
|
|
1332
|
+
getDefaultAgentId() {
|
|
1333
|
+
return normalizeOcuClawDefaultAgent(
|
|
1334
|
+
ocuClawSettingsStore.getSnapshot().defaultAgent,
|
|
1335
|
+
);
|
|
1336
|
+
},
|
|
1168
1337
|
isPinnedFirstUserMessageKey(sessionKey) {
|
|
1169
1338
|
const normalizedSessionKey = normalizeEvenAiSessionKeyForLookup(sessionKey);
|
|
1170
1339
|
if (!normalizedSessionKey) {
|
|
@@ -1258,13 +1427,6 @@ function createRelay(opts) {
|
|
|
1258
1427
|
}
|
|
1259
1428
|
}
|
|
1260
1429
|
|
|
1261
|
-
// TTL fallback for set_session_title activity label. The tool itself
|
|
1262
|
-
// completes in <50ms but its label can linger if no follow-up activity
|
|
1263
|
-
// arrives (e.g. agent streams a response directly after, with no
|
|
1264
|
-
// intervening activity event). After 1s, synthesize a thinking-status
|
|
1265
|
-
// activity with no tool/label so the renderer falls back to the bare
|
|
1266
|
-
// animated spinner. Any real activity arriving in the meantime cancels
|
|
1267
|
-
// the timer.
|
|
1268
1430
|
const SESSION_TITLE_STATUS_FALLBACK_MS = 1500;
|
|
1269
1431
|
let sessionTitleStatusFallbackTimer = null;
|
|
1270
1432
|
|
|
@@ -1339,6 +1501,20 @@ function createRelay(opts) {
|
|
|
1339
1501
|
return snapshot;
|
|
1340
1502
|
}
|
|
1341
1503
|
|
|
1504
|
+
function broadcastAgentsCatalog(snapshot) {
|
|
1505
|
+
if (!server || !handler || typeof handler.formatAgentsCatalog !== "function") {
|
|
1506
|
+
return snapshot;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
const agents =
|
|
1510
|
+
snapshot && Array.isArray(snapshot.agents) ? snapshot.agents : [];
|
|
1511
|
+
if (agents.length === 0 && !(snapshot && snapshot.unsupported)) {
|
|
1512
|
+
return snapshot;
|
|
1513
|
+
}
|
|
1514
|
+
server.broadcast(handler.formatAgentsCatalog(snapshot || {}));
|
|
1515
|
+
return snapshot;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1342
1518
|
const appClientDisconnectHandlers = new Set();
|
|
1343
1519
|
function onAppClientDisconnect(handler) {
|
|
1344
1520
|
if (typeof handler !== "function") return () => {};
|
|
@@ -1383,9 +1559,7 @@ function createRelay(opts) {
|
|
|
1383
1559
|
if (typeof patch.title === "string") cleanPatch.title = patch.title;
|
|
1384
1560
|
if (typeof patch.body === "string") cleanPatch.body = patch.body;
|
|
1385
1561
|
if (Array.isArray(patch.items)) {
|
|
1386
|
-
|
|
1387
|
-
// {label, body} objects (list_with_details detail-body ticks). Keep both
|
|
1388
|
-
// shapes; drop anything malformed (no string, no string label).
|
|
1562
|
+
|
|
1389
1563
|
cleanPatch.items = patch.items
|
|
1390
1564
|
.map((i) => {
|
|
1391
1565
|
if (typeof i === "string") return i;
|
|
@@ -1559,11 +1733,9 @@ function createRelay(opts) {
|
|
|
1559
1733
|
);
|
|
1560
1734
|
|
|
1561
1735
|
return maybeSeedOcuClawSessionConfig(resolvedSessionKey).then(() => {
|
|
1562
|
-
|
|
1563
|
-
// session busy so a racing glasses wake is absorbed (§2.6c).
|
|
1736
|
+
|
|
1564
1737
|
agentTurnTracker.markBusy(resolvedSessionKey);
|
|
1565
|
-
|
|
1566
|
-
// model tokens on large histories.
|
|
1738
|
+
|
|
1567
1739
|
const upstreamPromise = gatewayBridge.sendMessage(
|
|
1568
1740
|
text,
|
|
1569
1741
|
resolvedSessionKey,
|
|
@@ -1571,11 +1743,7 @@ function createRelay(opts) {
|
|
|
1571
1743
|
{
|
|
1572
1744
|
...stableSendOptions(
|
|
1573
1745
|
resolvedSessionKey,
|
|
1574
|
-
|
|
1575
|
-
// (resolveSessionCanonicalKey is async). Use the sessionKey as the
|
|
1576
|
-
// snapshot's id; the sessionId-mismatch guard is therefore a no-op,
|
|
1577
|
-
// and new-session safety rests on logical-session-end eviction
|
|
1578
|
-
// (onNewSession / onNewChat / onDeleteSessions evict the snapshot).
|
|
1746
|
+
|
|
1579
1747
|
resolvedSessionKey,
|
|
1580
1748
|
clientDisplaySignals,
|
|
1581
1749
|
),
|
|
@@ -1677,6 +1845,107 @@ function createRelay(opts) {
|
|
|
1677
1845
|
});
|
|
1678
1846
|
}
|
|
1679
1847
|
|
|
1848
|
+
function dispatchOcuClawSessionAbort(params = {}) {
|
|
1849
|
+
const requestId = params.requestId;
|
|
1850
|
+
const sessionKey =
|
|
1851
|
+
typeof params.sessionKey === "string" && params.sessionKey.trim()
|
|
1852
|
+
? params.sessionKey.trim()
|
|
1853
|
+
: sessionService.ensureSessionKey();
|
|
1854
|
+
emitDebug(
|
|
1855
|
+
"relay.protocol",
|
|
1856
|
+
"session_abort_requested",
|
|
1857
|
+
"info",
|
|
1858
|
+
{ sessionKey },
|
|
1859
|
+
() => ({ requestId }),
|
|
1860
|
+
);
|
|
1861
|
+
return gatewayBridge.request("sessions.abort", { key: sessionKey }).then(
|
|
1862
|
+
(result) => ({
|
|
1863
|
+
status: "accepted",
|
|
1864
|
+
...(result && typeof result === "object" ? result : {}),
|
|
1865
|
+
}),
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
function dispatchOcuClawSessionSteer(params = {}) {
|
|
1870
|
+
const requestId = params.requestId;
|
|
1871
|
+
const steerStartedAt = Date.now();
|
|
1872
|
+
const sessionKey =
|
|
1873
|
+
typeof params.sessionKey === "string" && params.sessionKey.trim()
|
|
1874
|
+
? params.sessionKey.trim()
|
|
1875
|
+
: sessionService.ensureSessionKey();
|
|
1876
|
+
const message = typeof params.message === "string" ? params.message : "";
|
|
1877
|
+
const attachment = params.attachment || null;
|
|
1878
|
+
const gatewayAttachment = buildGatewayAttachment(attachment);
|
|
1879
|
+
const request = {
|
|
1880
|
+
key: sessionKey,
|
|
1881
|
+
message,
|
|
1882
|
+
idempotencyKey: requestId,
|
|
1883
|
+
};
|
|
1884
|
+
if (gatewayAttachment) {
|
|
1885
|
+
request.attachments = [gatewayAttachment];
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
sessionService.recordFirstSentUserMessage(sessionKey, message);
|
|
1889
|
+
sessionService.invalidateSessionsCache();
|
|
1890
|
+
agentTurnTracker.markBusy(sessionKey);
|
|
1891
|
+
emitDebug(
|
|
1892
|
+
"relay.protocol",
|
|
1893
|
+
"session_steer_requested",
|
|
1894
|
+
"info",
|
|
1895
|
+
{ sessionKey },
|
|
1896
|
+
() => ({
|
|
1897
|
+
requestId,
|
|
1898
|
+
messageChars: message.length,
|
|
1899
|
+
hasAttachment: !!attachment,
|
|
1900
|
+
}),
|
|
1901
|
+
);
|
|
1902
|
+
|
|
1903
|
+
const diagnostic = {
|
|
1904
|
+
messageId: requestId,
|
|
1905
|
+
sessionKey,
|
|
1906
|
+
source: "phone_ui_replace",
|
|
1907
|
+
textChars: message.length,
|
|
1908
|
+
hasAttachment: !!attachment,
|
|
1909
|
+
attachmentBytes:
|
|
1910
|
+
attachment && Number.isFinite(attachment.sizeBytes)
|
|
1911
|
+
? Math.floor(attachment.sizeBytes)
|
|
1912
|
+
: null,
|
|
1913
|
+
};
|
|
1914
|
+
|
|
1915
|
+
return maybeSeedOcuClawSessionConfig(sessionKey)
|
|
1916
|
+
.then(() => gatewayBridge.request("sessions.steer", request, {
|
|
1917
|
+
expectFinal: false,
|
|
1918
|
+
diagnostic,
|
|
1919
|
+
}))
|
|
1920
|
+
.then((result) => {
|
|
1921
|
+
const ackAt = Date.now();
|
|
1922
|
+
const runId = result && result.runId ? result.runId : null;
|
|
1923
|
+
if (runId && upstreamRuntime) {
|
|
1924
|
+
upstreamRuntime.trackAcceptedRun({
|
|
1925
|
+
runId,
|
|
1926
|
+
sessionKey,
|
|
1927
|
+
messageId: requestId,
|
|
1928
|
+
sendStartedAt: steerStartedAt,
|
|
1929
|
+
ackAt,
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
const userContent = buildLocalUserMessageContent(message, attachment);
|
|
1933
|
+
conversationState.addMessage("user", userContent);
|
|
1934
|
+
emitDebug(
|
|
1935
|
+
"openclaw.message",
|
|
1936
|
+
"user_message",
|
|
1937
|
+
"info",
|
|
1938
|
+
{ sessionKey },
|
|
1939
|
+
() => ({ text: message }),
|
|
1940
|
+
);
|
|
1941
|
+
broadcastPages();
|
|
1942
|
+
return {
|
|
1943
|
+
...(result && typeof result === "object" ? result : {}),
|
|
1944
|
+
status: "accepted",
|
|
1945
|
+
};
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1680
1949
|
function emitListenInterceptRecovery(params = {}) {
|
|
1681
1950
|
const connectedAppClients = server ? server.getConnectedAppCount() : 0;
|
|
1682
1951
|
if (!server || !handler) {
|
|
@@ -1707,9 +1976,6 @@ function createRelay(opts) {
|
|
|
1707
1976
|
server.broadcast(handler.formatEvenAiListenIntercepted(sessionKey));
|
|
1708
1977
|
}
|
|
1709
1978
|
|
|
1710
|
-
// --- Downstream handler ---
|
|
1711
|
-
|
|
1712
|
-
/** @type {ReturnType<typeof createRelayWorkerSupervisor>|null} */
|
|
1713
1979
|
let server = null;
|
|
1714
1980
|
let evenAiEndpoint = null;
|
|
1715
1981
|
let evenAiRouter = null;
|
|
@@ -1733,12 +1999,6 @@ function createRelay(opts) {
|
|
|
1733
1999
|
return { ok: true, enabled, persisted, persistedPath: liveUiTraceFlagPath };
|
|
1734
2000
|
}
|
|
1735
2001
|
|
|
1736
|
-
// Persist the current debug-store arm to debug-arm.json. Mirrors the
|
|
1737
|
-
// applyTraceLogSet writeFileSync above (plain, non-atomic): a partial/corrupt
|
|
1738
|
-
// write degrades to an empty arm on next boot — acceptable, the nothing-armed
|
|
1739
|
-
// warning catches it. getSnapshot().enabled is already pruned of expired
|
|
1740
|
-
// categories, so the persisted JSON never holds an expired entry. Never throws
|
|
1741
|
-
// into the caller.
|
|
1742
2002
|
function persistDebugArm() {
|
|
1743
2003
|
if (!debugArmStatePath) return false;
|
|
1744
2004
|
try {
|
|
@@ -1756,9 +2016,7 @@ function createRelay(opts) {
|
|
|
1756
2016
|
if (!result.ok) {
|
|
1757
2017
|
throw new Error(result.error || "debug-set failed");
|
|
1758
2018
|
}
|
|
1759
|
-
|
|
1760
|
-
// on-disk arm always tracks live state and a deliberately-cleared arm is not
|
|
1761
|
-
// resurrected on the next restart.
|
|
2019
|
+
|
|
1762
2020
|
persistDebugArm();
|
|
1763
2021
|
emitDebug(
|
|
1764
2022
|
"relay.protocol",
|
|
@@ -1784,15 +2042,7 @@ function createRelay(opts) {
|
|
|
1784
2042
|
if (kind === "status") return statusRevision;
|
|
1785
2043
|
return null;
|
|
1786
2044
|
},
|
|
1787
|
-
|
|
1788
|
-
* Forward a user message to the upstream OpenClaw agent.
|
|
1789
|
-
*
|
|
1790
|
-
* @param {string} id - Message ID
|
|
1791
|
-
* @param {string} text - User message text
|
|
1792
|
-
* @param {string|null} sessionKey - Session key
|
|
1793
|
-
* @param {object|null} attachment - Optional image attachment payload
|
|
1794
|
-
* @returns {Promise}
|
|
1795
|
-
*/
|
|
2045
|
+
|
|
1796
2046
|
onSend(id, text, sessionKey, attachment, clientDisplaySignals) {
|
|
1797
2047
|
return dispatchOcuClawUserSend({
|
|
1798
2048
|
id,
|
|
@@ -1803,6 +2053,17 @@ function createRelay(opts) {
|
|
|
1803
2053
|
source: "phone_ui",
|
|
1804
2054
|
});
|
|
1805
2055
|
},
|
|
2056
|
+
onAbortSession({ requestId, sessionKey }) {
|
|
2057
|
+
return dispatchOcuClawSessionAbort({ requestId, sessionKey });
|
|
2058
|
+
},
|
|
2059
|
+
onSteerSession({ requestId, sessionKey, message, attachment }) {
|
|
2060
|
+
return dispatchOcuClawSessionSteer({
|
|
2061
|
+
requestId,
|
|
2062
|
+
sessionKey,
|
|
2063
|
+
message,
|
|
2064
|
+
attachment,
|
|
2065
|
+
});
|
|
2066
|
+
},
|
|
1806
2067
|
onGlassesUiResult(frame) {
|
|
1807
2068
|
emitDebug(
|
|
1808
2069
|
"glasses.lifecycle",
|
|
@@ -1893,19 +2154,89 @@ function createRelay(opts) {
|
|
|
1893
2154
|
}
|
|
1894
2155
|
});
|
|
1895
2156
|
},
|
|
2157
|
+
|
|
2158
|
+
onDebugBundleRequest(clientId, msg) {
|
|
2159
|
+
|
|
2160
|
+
const reportedClientVersion = (() => {
|
|
2161
|
+
try {
|
|
2162
|
+
const snap =
|
|
2163
|
+
server && typeof server.getReadinessSnapshot === "function"
|
|
2164
|
+
? server.getReadinessSnapshot()
|
|
2165
|
+
: null;
|
|
2166
|
+
const entry =
|
|
2167
|
+
snap && Array.isArray(snap.clients)
|
|
2168
|
+
? snap.clients.find((c) => c.clientId === clientId)
|
|
2169
|
+
: null;
|
|
2170
|
+
const v = entry && typeof entry.clientVersion === "string" ? entry.clientVersion.trim() : "";
|
|
2171
|
+
return v.length ? v : null;
|
|
2172
|
+
} catch {
|
|
2173
|
+
return null;
|
|
2174
|
+
}
|
|
2175
|
+
})();
|
|
2176
|
+
const deps = {
|
|
2177
|
+
gatesOn: () => externalDebugToolsEnabled && allowDebugUpload,
|
|
2178
|
+
dump: (query) => debugStore.dump(query),
|
|
2179
|
+
|
|
2180
|
+
preset:
|
|
2181
|
+
opts.debugUploadCapturePreset &&
|
|
2182
|
+
Array.isArray(opts.debugUploadCapturePreset) &&
|
|
2183
|
+
opts.debugUploadCapturePreset.length
|
|
2184
|
+
? opts.debugUploadCapturePreset
|
|
2185
|
+
: UPLOAD_CAPTURE_PRESET,
|
|
2186
|
+
|
|
2187
|
+
build: {
|
|
2188
|
+
clientVersion: reportedClientVersion,
|
|
2189
|
+
requiresClientVersion: pluginVersionService.getRequiresClientVersion(),
|
|
2190
|
+
pluginVersion: pluginVersionService.getPluginVersion(),
|
|
2191
|
+
openclawVersion: pluginVersionService.getOpenClawHostVersion(),
|
|
2192
|
+
distHash: pluginVersionService.getDistHash(),
|
|
2193
|
+
},
|
|
2194
|
+
idSalt: debugBundleIdSalt,
|
|
2195
|
+
maxZipBytes: debugUploadMaxZipBytes,
|
|
2196
|
+
chunkBytes: 64000,
|
|
2197
|
+
send: (id, frame) => {
|
|
2198
|
+
if (server) server.unicast(id, JSON.stringify(frame));
|
|
2199
|
+
},
|
|
2200
|
+
|
|
2201
|
+
emit: (event, data) =>
|
|
2202
|
+
emitDebug("relay.operation", event, "debug", {}, () => data),
|
|
2203
|
+
newBundleId: () => crypto.randomUUID(),
|
|
2204
|
+
cachePut: (id, e) => bundleCache.put(id, e),
|
|
2205
|
+
now: () => Date.now(),
|
|
2206
|
+
};
|
|
2207
|
+
return Promise.resolve(handleDebugBundleRequest(deps, clientId, msg)).catch(
|
|
2208
|
+
(err) => {
|
|
2209
|
+
logger.error(
|
|
2210
|
+
`[relay] debug-bundle-request failed: ${err && err.message ? err.message : err}`,
|
|
2211
|
+
);
|
|
2212
|
+
},
|
|
2213
|
+
);
|
|
2214
|
+
},
|
|
2215
|
+
onDebugBundleSave(clientId, msg) {
|
|
2216
|
+
return Promise.resolve(handleDebugBundleSave({
|
|
2217
|
+
gatesOn: () => externalDebugToolsEnabled && allowDebugUpload,
|
|
2218
|
+
cacheGet: (id) => bundleCache.get(id),
|
|
2219
|
+
saveBundle: (a) => saveBundleToDisk({ ...a, saveDir: resolveSaveDir(), fs, path }),
|
|
2220
|
+
now: () => Date.now(),
|
|
2221
|
+
send: (id, frame) => { if (server) server.unicast(id, JSON.stringify(frame)); },
|
|
2222
|
+
emit: (event, data) => emitDebug("relay.operation", event, "debug", {}, () => data),
|
|
2223
|
+
}, clientId, msg)).catch((err) => {
|
|
2224
|
+
logger.error(`[relay] debug-bundle-save failed: ${err && err.message ? err.message : err}`);
|
|
2225
|
+
});
|
|
2226
|
+
},
|
|
2227
|
+
onDebugBundleFetch(clientId, msg) {
|
|
2228
|
+
return Promise.resolve(handleDebugBundleFetch({
|
|
2229
|
+
gatesOn: () => externalDebugToolsEnabled && allowDebugUpload,
|
|
2230
|
+
cacheGet: (id) => bundleCache.get(id),
|
|
2231
|
+
chunkBytes: 64000,
|
|
2232
|
+
send: (id, frame) => { if (server) server.unicast(id, JSON.stringify(frame)); },
|
|
2233
|
+
emit: (event, data) => emitDebug("relay.operation", event, "debug", {}, () => data),
|
|
2234
|
+
}, clientId, msg)).catch((err) => {
|
|
2235
|
+
logger.error(`[relay] debug-bundle-fetch failed: ${err && err.message ? err.message : err}`);
|
|
2236
|
+
});
|
|
2237
|
+
},
|
|
1896
2238
|
operationRegistry: relayOperationRegistry,
|
|
1897
2239
|
|
|
1898
|
-
/**
|
|
1899
|
-
* Inject a fake assistant message into conversation state.
|
|
1900
|
-
*
|
|
1901
|
-
* The sender name is used as a temporary agent name prefix for
|
|
1902
|
-
* this message. If no agent identity has been established yet,
|
|
1903
|
-
* the sender becomes the default agent name going forward.
|
|
1904
|
-
*
|
|
1905
|
-
* @param {string} sender - Display name for the simulated message
|
|
1906
|
-
* @param {string} text - Message text
|
|
1907
|
-
* @returns {Array<{content: string, subPage: [number, number]|null}>}
|
|
1908
|
-
*/
|
|
1909
2240
|
onSimulate(sender, text) {
|
|
1910
2241
|
emitDebug(
|
|
1911
2242
|
"relay.protocol",
|
|
@@ -1917,7 +2248,7 @@ function createRelay(opts) {
|
|
|
1917
2248
|
textChars: typeof text === "string" ? text.length : 0,
|
|
1918
2249
|
}),
|
|
1919
2250
|
);
|
|
1920
|
-
|
|
2251
|
+
|
|
1921
2252
|
conversationState.addMessage("assistant", [{ type: "text", text }], sender || "Simulator");
|
|
1922
2253
|
|
|
1923
2254
|
const pages = conversationState.getPages();
|
|
@@ -2035,12 +2366,6 @@ function createRelay(opts) {
|
|
|
2035
2366
|
});
|
|
2036
2367
|
},
|
|
2037
2368
|
|
|
2038
|
-
/**
|
|
2039
|
-
* Clear conversation state, reset cached pages, and send /new to OpenClaw.
|
|
2040
|
-
* Legacy support: delegates to newSession.
|
|
2041
|
-
*
|
|
2042
|
-
* @returns {Promise<Array>} Empty pages array
|
|
2043
|
-
*/
|
|
2044
2369
|
onNewChat() {
|
|
2045
2370
|
emitDebug(
|
|
2046
2371
|
"relay.session",
|
|
@@ -2054,15 +2379,10 @@ function createRelay(opts) {
|
|
|
2054
2379
|
}
|
|
2055
2380
|
sessionService.invalidateSessionsCache();
|
|
2056
2381
|
resetActivityStatusAdapter();
|
|
2057
|
-
|
|
2058
|
-
// a deferred addMessage firing after the clear repopulates the fresh chat
|
|
2059
|
-
// (the 2026-05-15 canary-pollution mechanism).
|
|
2382
|
+
|
|
2060
2383
|
clearSimulateStreamTimersForSession(sessionService.ensureSessionKey());
|
|
2061
2384
|
conversationState.clear();
|
|
2062
|
-
|
|
2063
|
-
// Channel-1 snapshot must be dropped or the next send serves a stale prompt,
|
|
2064
|
-
// and ALL other per-session state keyed to it (title + upstream name,
|
|
2065
|
-
// toggles, distiller budget, first-user marker) must be cleared too.
|
|
2385
|
+
|
|
2066
2386
|
const newChatSessionKey = sessionService.ensureSessionKey();
|
|
2067
2387
|
stablePromptSnapshots.evict(newChatSessionKey);
|
|
2068
2388
|
sessionService.clearLogicalSessionState(newChatSessionKey);
|
|
@@ -2072,12 +2392,7 @@ function createRelay(opts) {
|
|
|
2072
2392
|
const pages = conversationState.getPages();
|
|
2073
2393
|
cachePages(pages);
|
|
2074
2394
|
if (upstreamRuntime && upstreamRuntime.isConnected()) {
|
|
2075
|
-
|
|
2076
|
-
// changing currentSessionKey, so it must NOT elicit a welcome turn here:
|
|
2077
|
-
// the turn's events would carry "main" and be dropped by isCurrentSession()
|
|
2078
|
-
// whenever the active session is an ocuclaw:* key. The welcome restore for
|
|
2079
|
-
// the real glasses paths lives in newSession() (New) and onSlashCommand
|
|
2080
|
-
// "/reset" (Reset). Unifying onNewChat onto newSession() is Phase-2 work.
|
|
2395
|
+
|
|
2081
2396
|
gatewayBridge.sendMessage("/new", "main").catch((err) => {
|
|
2082
2397
|
logger.error(`[relay] Failed to send /new: ${err.message}`);
|
|
2083
2398
|
});
|
|
@@ -2103,16 +2418,10 @@ function createRelay(opts) {
|
|
|
2103
2418
|
},
|
|
2104
2419
|
|
|
2105
2420
|
async onNewSession() {
|
|
2106
|
-
|
|
2107
|
-
// BEFORE the new key is minted — a deferred addMessage firing after the
|
|
2108
|
-
// switch would repopulate the fresh session's shared conversation view.
|
|
2421
|
+
|
|
2109
2422
|
clearSimulateStreamTimersForSession(sessionService.ensureSessionKey());
|
|
2110
2423
|
const result = await sessionService.newSession();
|
|
2111
|
-
|
|
2112
|
-
// has no snapshot yet). Do NOT touch the previous key: that session stays
|
|
2113
|
-
// resumable via onSwitchSession, and dropping its frozen snapshot would
|
|
2114
|
-
// recompute — and churn — Channel 1 if the user switches back to it. The
|
|
2115
|
-
// previous session's snapshot is released by delete or the TTL sweep.
|
|
2424
|
+
|
|
2116
2425
|
if (result && typeof result.sessionKey === "string" && result.sessionKey.trim()) {
|
|
2117
2426
|
stablePromptSnapshots.evict(result.sessionKey);
|
|
2118
2427
|
sessionService.clearDisplayToggleStates(result.sessionKey);
|
|
@@ -2147,6 +2456,20 @@ function createRelay(opts) {
|
|
|
2147
2456
|
: Promise.resolve({ skills: [], fetchedAtMs: Date.now(), stale: true });
|
|
2148
2457
|
},
|
|
2149
2458
|
|
|
2459
|
+
onGetAgentsCatalog() {
|
|
2460
|
+
return upstreamRuntime
|
|
2461
|
+
? upstreamRuntime.getAgentsCatalogSnapshot()
|
|
2462
|
+
: Promise.resolve({
|
|
2463
|
+
agents: [],
|
|
2464
|
+
defaultId: null,
|
|
2465
|
+
mainKey: null,
|
|
2466
|
+
scope: null,
|
|
2467
|
+
fetchedAtMs: Date.now(),
|
|
2468
|
+
stale: true,
|
|
2469
|
+
unsupported: true,
|
|
2470
|
+
});
|
|
2471
|
+
},
|
|
2472
|
+
|
|
2150
2473
|
onGetProviderUsageSnapshot() {
|
|
2151
2474
|
return upstreamRuntime
|
|
2152
2475
|
? upstreamRuntime.getProviderUsageSnapshot()
|
|
@@ -2187,6 +2510,32 @@ function createRelay(opts) {
|
|
|
2187
2510
|
return result;
|
|
2188
2511
|
},
|
|
2189
2512
|
|
|
2513
|
+
onSetSessionAgent(patch) {
|
|
2514
|
+
const sessionKey = sessionService.ensureSessionKey();
|
|
2515
|
+
const result = sessionService.setSessionAgentId(
|
|
2516
|
+
sessionKey,
|
|
2517
|
+
(patch && patch.agentId) || "",
|
|
2518
|
+
);
|
|
2519
|
+
if (!result || result.ok !== true) {
|
|
2520
|
+
return {
|
|
2521
|
+
status: "rejected",
|
|
2522
|
+
error: (result && result.reason) || "invalid session agent",
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
if (typeof sessionService.primeSessionModelConfig === "function") {
|
|
2527
|
+
const config = sessionService.primeSessionModelConfig(sessionKey, {});
|
|
2528
|
+
if (config && isActiveSessionModelConfig(config)) {
|
|
2529
|
+
currentSessionModelConfigSnapshot = config;
|
|
2530
|
+
if (server) {
|
|
2531
|
+
server.broadcast(handler.formatSessionModelConfig(config));
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
broadcastSessions();
|
|
2536
|
+
return { status: "accepted" };
|
|
2537
|
+
},
|
|
2538
|
+
|
|
2190
2539
|
onGetEvenAiSettings() {
|
|
2191
2540
|
return evenAiSettingsStore.getSnapshot();
|
|
2192
2541
|
},
|
|
@@ -2236,23 +2585,15 @@ function createRelay(opts) {
|
|
|
2236
2585
|
);
|
|
2237
2586
|
broadcastPages();
|
|
2238
2587
|
}
|
|
2239
|
-
|
|
2240
|
-
// key (distinct from an automatic CLI session reset, which keeps the same
|
|
2241
|
-
// logical session and must survive). Drop the frozen Channel-1 snapshot so
|
|
2242
|
-
// the next real message recomputes it for the fresh conversation; otherwise
|
|
2243
|
-
// the old conversation's prompt + display start-state bleed into the new one.
|
|
2588
|
+
|
|
2244
2589
|
if (command === "/new" || command === "/reset") {
|
|
2245
2590
|
const resetKey = sessionService.ensureSessionKey();
|
|
2246
2591
|
stablePromptSnapshots.evict(resetKey);
|
|
2247
|
-
|
|
2248
|
-
// name, toggles, distiller budget, first-user marker) so nothing from the
|
|
2249
|
-
// old conversation bleeds into the fresh one.
|
|
2592
|
+
|
|
2250
2593
|
sessionService.clearLogicalSessionState(resetKey);
|
|
2251
2594
|
}
|
|
2252
2595
|
if (upstreamRuntime && upstreamRuntime.isConnected()) {
|
|
2253
|
-
|
|
2254
|
-
// (fast-reset). Append the greeting prompt so Reset gets the same
|
|
2255
|
-
// welcome as New. Other slash commands forward verbatim.
|
|
2596
|
+
|
|
2256
2597
|
const outboundCommand =
|
|
2257
2598
|
command === "/reset"
|
|
2258
2599
|
? `/reset ${NEW_SESSION_GREETING_PROMPT}`
|
|
@@ -2265,9 +2606,6 @@ function createRelay(opts) {
|
|
|
2265
2606
|
return Promise.resolve();
|
|
2266
2607
|
},
|
|
2267
2608
|
|
|
2268
|
-
/**
|
|
2269
|
-
* @returns {boolean} Whether upstream is connected.
|
|
2270
|
-
*/
|
|
2271
2609
|
isUpstreamConnected() {
|
|
2272
2610
|
return true;
|
|
2273
2611
|
},
|
|
@@ -2318,6 +2656,10 @@ function createRelay(opts) {
|
|
|
2318
2656
|
return mintSonioxTemporaryKey(clientId, request);
|
|
2319
2657
|
},
|
|
2320
2658
|
|
|
2659
|
+
onRequestCartesiaAccessToken(clientId, request) {
|
|
2660
|
+
return mintCartesiaAccessToken(clientId, request);
|
|
2661
|
+
},
|
|
2662
|
+
|
|
2321
2663
|
onDebugSet(clientId, request) {
|
|
2322
2664
|
return applyDebugSet(clientId, request);
|
|
2323
2665
|
},
|
|
@@ -2565,14 +2907,7 @@ function createRelay(opts) {
|
|
|
2565
2907
|
},
|
|
2566
2908
|
|
|
2567
2909
|
onAutomationState(clientId, request) {
|
|
2568
|
-
|
|
2569
|
-
// client via the readiness snapshot, then return a dispatch envelope
|
|
2570
|
-
// that downstream-handler.handleAutomationState wraps into
|
|
2571
|
-
// `automationStateRequest`. Without this callback wired, the handler
|
|
2572
|
-
// returns null and the request is silently dropped at the relay —
|
|
2573
|
-
// simctl/debugctl times out with no failure response, no trace event,
|
|
2574
|
-
// no outbox drop. The lack of wiring was found 2026-05-28 while
|
|
2575
|
-
// validating the streaming-thinking-emoji-demotion fix on the sim.
|
|
2910
|
+
|
|
2576
2911
|
const now = Date.now();
|
|
2577
2912
|
const requestId =
|
|
2578
2913
|
(typeof request.requestId === "string" && request.requestId.trim()) ||
|
|
@@ -2601,11 +2936,7 @@ function createRelay(opts) {
|
|
|
2601
2936
|
targetEntry && typeof targetEntry.clientId === "string"
|
|
2602
2937
|
? targetEntry.clientId
|
|
2603
2938
|
: null;
|
|
2604
|
-
|
|
2605
|
-
// cannot answer an automation state request; forwarding anyway would
|
|
2606
|
-
// park the request in pendingAutomationStateRequests with no reply.
|
|
2607
|
-
// Same predicate as the downstream readiness gate; this wired callback
|
|
2608
|
-
// bypasses the normal dispatch path.
|
|
2939
|
+
|
|
2609
2940
|
const readinessPublished =
|
|
2610
2941
|
!!(
|
|
2611
2942
|
targetEntry &&
|
|
@@ -2692,13 +3023,8 @@ function createRelay(opts) {
|
|
|
2692
3023
|
},
|
|
2693
3024
|
});
|
|
2694
3025
|
|
|
2695
|
-
// --- Worker supervisor ---
|
|
2696
|
-
|
|
2697
3026
|
const pluginVersionService = createPluginVersionService();
|
|
2698
3027
|
|
|
2699
|
-
// Roadmap 4a: latch the worker's per-heartbeat send-buffer pressure counts
|
|
2700
|
-
// into the boolean the glasses-ui paint-floor shed queries
|
|
2701
|
-
// (isGlassesSendBufferOverHighWater on the relay API / relay-service facade).
|
|
2702
3028
|
const glassesBackpressureLatch = createGlassesBackpressureLatch({
|
|
2703
3029
|
emitDebug: (event, severity, data) =>
|
|
2704
3030
|
emitDebug("relay.health", event, severity, null, () => data || {}),
|
|
@@ -2759,8 +3085,6 @@ function createRelay(opts) {
|
|
|
2759
3085
|
},
|
|
2760
3086
|
});
|
|
2761
3087
|
|
|
2762
|
-
// --- Helpers ---
|
|
2763
|
-
|
|
2764
3088
|
function buildStatusObject(options = {}) {
|
|
2765
3089
|
const includeDownstreamReadiness = options.includeDownstreamReadiness === true;
|
|
2766
3090
|
const status = {
|
|
@@ -2808,9 +3132,6 @@ function createRelay(opts) {
|
|
|
2808
3132
|
return cachedStatus;
|
|
2809
3133
|
}
|
|
2810
3134
|
|
|
2811
|
-
/**
|
|
2812
|
-
* Recompute pages from conversation state, cache, and broadcast.
|
|
2813
|
-
*/
|
|
2814
3135
|
function broadcastPages() {
|
|
2815
3136
|
const pages = conversationState.getPages();
|
|
2816
3137
|
const next = cachePages(pages);
|
|
@@ -2819,12 +3140,6 @@ function createRelay(opts) {
|
|
|
2819
3140
|
}
|
|
2820
3141
|
}
|
|
2821
3142
|
|
|
2822
|
-
/**
|
|
2823
|
-
* Fetch the latest sessions snapshot and broadcast it. Used after a session
|
|
2824
|
-
* title changes so connected clients refresh the title in the main webui
|
|
2825
|
-
* status row and Session Settings tab without waiting for a manual
|
|
2826
|
-
* session-list open.
|
|
2827
|
-
*/
|
|
2828
3143
|
function broadcastSessions() {
|
|
2829
3144
|
sessionService
|
|
2830
3145
|
.getSessions()
|
|
@@ -2842,10 +3157,6 @@ function createRelay(opts) {
|
|
|
2842
3157
|
});
|
|
2843
3158
|
}
|
|
2844
3159
|
|
|
2845
|
-
/**
|
|
2846
|
-
* Resolve the current Even AI sessions snapshot for unicast/broadcast.
|
|
2847
|
-
* Mirrors the shape that `formatEvenAiSessions` expects.
|
|
2848
|
-
*/
|
|
2849
3160
|
async function buildEvenAiSessionsSnapshot() {
|
|
2850
3161
|
const dedicatedKey =
|
|
2851
3162
|
evenAiRouter && typeof evenAiRouter.getDedicatedSessionKey === "function"
|
|
@@ -2910,9 +3221,6 @@ function createRelay(opts) {
|
|
|
2910
3221
|
});
|
|
2911
3222
|
}
|
|
2912
3223
|
|
|
2913
|
-
/**
|
|
2914
|
-
* Build, cache, and broadcast the current status.
|
|
2915
|
-
*/
|
|
2916
3224
|
function broadcastStatus() {
|
|
2917
3225
|
const next = cacheStatus(buildStatusObject());
|
|
2918
3226
|
if (next !== null) {
|
|
@@ -2945,6 +3253,7 @@ function createRelay(opts) {
|
|
|
2945
3253
|
broadcastStatus,
|
|
2946
3254
|
broadcastActivity,
|
|
2947
3255
|
broadcastProviderUsageSnapshot,
|
|
3256
|
+
broadcastAgentsCatalog,
|
|
2948
3257
|
operationRegistry: relayOperationRegistry,
|
|
2949
3258
|
getCurrentSessionModelConfigSnapshot() {
|
|
2950
3259
|
return currentSessionModelConfigSnapshot;
|
|
@@ -2962,10 +3271,6 @@ function createRelay(opts) {
|
|
|
2962
3271
|
fetchAgentAvatar: opts.fetchAgentAvatar,
|
|
2963
3272
|
});
|
|
2964
3273
|
|
|
2965
|
-
// Shared routing gate for session-scoped Even AI defaults (thinking seed,
|
|
2966
|
-
// fast-mode patch): never touch active-routed sessions; always seed fresh
|
|
2967
|
-
// background_new sessions; seed persistent background sessions only before
|
|
2968
|
-
// their first turn exists.
|
|
2969
3274
|
async function shouldSeedSessionScopedDefaultForRoute(route) {
|
|
2970
3275
|
const routingMode =
|
|
2971
3276
|
route && typeof route.routingMode === "string"
|
|
@@ -3074,6 +3379,29 @@ function createRelay(opts) {
|
|
|
3074
3379
|
);
|
|
3075
3380
|
return !!(result && result.status === "accepted");
|
|
3076
3381
|
},
|
|
3382
|
+
resolveAgentForRoute(params) {
|
|
3383
|
+
const route = params && params.route ? params.route : params;
|
|
3384
|
+
const routingMode =
|
|
3385
|
+
(route && typeof route.routingMode === "string"
|
|
3386
|
+
? route.routingMode.trim().toLowerCase()
|
|
3387
|
+
: "") || "active";
|
|
3388
|
+
const sessionKey =
|
|
3389
|
+
route && typeof route.sessionKey === "string"
|
|
3390
|
+
? route.sessionKey.trim()
|
|
3391
|
+
: "";
|
|
3392
|
+
|
|
3393
|
+
if (routingMode === "active") {
|
|
3394
|
+
return sessionKey ? sessionService.getSessionAgentId(sessionKey) : "";
|
|
3395
|
+
}
|
|
3396
|
+
const evenAiDefault = normalizeEvenAiDefaultAgent(
|
|
3397
|
+
evenAiSettingsStore.getSnapshot().defaultAgent,
|
|
3398
|
+
);
|
|
3399
|
+
if (sessionKey && evenAiDefault) {
|
|
3400
|
+
|
|
3401
|
+
sessionService.setSessionAgentId(sessionKey, evenAiDefault);
|
|
3402
|
+
}
|
|
3403
|
+
return evenAiDefault;
|
|
3404
|
+
},
|
|
3077
3405
|
onSessionActivated(route) {
|
|
3078
3406
|
if (!route || !route.sessionChanged) {
|
|
3079
3407
|
return;
|
|
@@ -3134,34 +3462,45 @@ function createRelay(opts) {
|
|
|
3134
3462
|
return true;
|
|
3135
3463
|
}
|
|
3136
3464
|
|
|
3137
|
-
// --- Public API ---
|
|
3138
|
-
|
|
3139
3465
|
relayApi = {
|
|
3140
|
-
|
|
3141
|
-
* Emit a glasses-UI surface-lifecycle event on the permanent
|
|
3142
|
-
* `glasses.lifecycle` debug category (nav reconcile + cron pause/resume/
|
|
3143
|
-
* tick). Recorded only when the category is enabled via debug-set. Wired
|
|
3144
|
-
* through the relay-service facade into the glasses-ui tool handler + cron
|
|
3145
|
-
* engine. See docs/superpowers/findings/2026-05-30-glasses-ui-phase4-hardware.md.
|
|
3146
|
-
*/
|
|
3466
|
+
|
|
3147
3467
|
emitGlassesUiLifecycle(event, severity, data) {
|
|
3148
3468
|
emitDebug("glasses.lifecycle", event, severity, {}, () => data || {});
|
|
3149
3469
|
},
|
|
3150
|
-
|
|
3151
|
-
* Start the upstream OpenClaw connection.
|
|
3152
|
-
* The downstream server is already listening from construction.
|
|
3153
|
-
*/
|
|
3470
|
+
|
|
3154
3471
|
start() {
|
|
3155
|
-
|
|
3472
|
+
|
|
3473
|
+
if (!bundleCacheSweepTimer) {
|
|
3474
|
+
bundleCacheSweepTimer = setInterval(() => bundleCache.sweep(), 60_000);
|
|
3475
|
+
if (typeof bundleCacheSweepTimer.unref === "function") bundleCacheSweepTimer.unref();
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3156
3478
|
if (!stablePromptSweepTimer) {
|
|
3157
3479
|
stablePromptSweepTimer = setInterval(
|
|
3158
3480
|
() => stablePromptSnapshots.sweep(),
|
|
3159
|
-
60 * 60 * 1000,
|
|
3481
|
+
60 * 60 * 1000,
|
|
3160
3482
|
);
|
|
3161
3483
|
if (typeof stablePromptSweepTimer.unref === "function") {
|
|
3162
3484
|
stablePromptSweepTimer.unref();
|
|
3163
3485
|
}
|
|
3164
3486
|
}
|
|
3487
|
+
|
|
3488
|
+
if (!uploadCaptureArmingDisposer) {
|
|
3489
|
+
uploadCaptureArmingDisposer = startUploadCaptureArming({
|
|
3490
|
+
gatesOn: () => externalDebugToolsEnabled && allowDebugUpload,
|
|
3491
|
+
armCategories: (cats, ttlMs) =>
|
|
3492
|
+
applyDebugSet("upload-capture-arming", { enable: cats, ttlMs }),
|
|
3493
|
+
maxTtlMs: debugStore.getConfig().maxTtlMs,
|
|
3494
|
+
|
|
3495
|
+
preset: opts.debugUploadCapturePreset,
|
|
3496
|
+
onArmError: (err) =>
|
|
3497
|
+
logger.warn(
|
|
3498
|
+
`[relay] upload-capture arming failed (preset override?): ${err && err.message}`,
|
|
3499
|
+
),
|
|
3500
|
+
setInterval,
|
|
3501
|
+
clearInterval,
|
|
3502
|
+
});
|
|
3503
|
+
}
|
|
3165
3504
|
const startGateway = () => Promise.resolve(gatewayBridge.start()).then(() => {
|
|
3166
3505
|
prefetchSonioxModels("relay_start").catch((err) => {
|
|
3167
3506
|
logger.warn(`[relay] Soniox models prefetch failed: ${err.message}`);
|
|
@@ -3176,17 +3515,20 @@ function createRelay(opts) {
|
|
|
3176
3515
|
return startGateway();
|
|
3177
3516
|
},
|
|
3178
3517
|
|
|
3179
|
-
/**
|
|
3180
|
-
* Stop the upstream connection and shut down the downstream server.
|
|
3181
|
-
*
|
|
3182
|
-
* @returns {Promise<void>}
|
|
3183
|
-
*/
|
|
3184
3518
|
stop() {
|
|
3185
3519
|
clearSimulateStreamTimers();
|
|
3520
|
+
if (bundleCacheSweepTimer) {
|
|
3521
|
+
clearInterval(bundleCacheSweepTimer);
|
|
3522
|
+
bundleCacheSweepTimer = null;
|
|
3523
|
+
}
|
|
3186
3524
|
if (stablePromptSweepTimer) {
|
|
3187
3525
|
clearInterval(stablePromptSweepTimer);
|
|
3188
3526
|
stablePromptSweepTimer = null;
|
|
3189
3527
|
}
|
|
3528
|
+
if (uploadCaptureArmingDisposer) {
|
|
3529
|
+
uploadCaptureArmingDisposer();
|
|
3530
|
+
uploadCaptureArmingDisposer = null;
|
|
3531
|
+
}
|
|
3190
3532
|
if (evenAiEndpoint) {
|
|
3191
3533
|
evenAiEndpoint.close();
|
|
3192
3534
|
}
|
|
@@ -3213,7 +3555,6 @@ function createRelay(opts) {
|
|
|
3213
3555
|
|
|
3214
3556
|
handleBufferedEvenAiHttpRequest,
|
|
3215
3557
|
|
|
3216
|
-
/** The downstream server instance. */
|
|
3217
3558
|
get server() {
|
|
3218
3559
|
return server;
|
|
3219
3560
|
},
|
|
@@ -3274,7 +3615,6 @@ function createRelay(opts) {
|
|
|
3274
3615
|
return sessionService.getDisplayCurrentStates(sessionKey);
|
|
3275
3616
|
},
|
|
3276
3617
|
|
|
3277
|
-
// Accessors used by the session-title distiller sidecar.
|
|
3278
3618
|
getSessionTitleRecord(sessionKey) {
|
|
3279
3619
|
return sessionService.getSessionTitleRecord(sessionKey);
|
|
3280
3620
|
},
|
|
@@ -3287,12 +3627,7 @@ function createRelay(opts) {
|
|
|
3287
3627
|
getDistillerBudget() {
|
|
3288
3628
|
return sessionService.getDistillerBudget();
|
|
3289
3629
|
},
|
|
3290
|
-
|
|
3291
|
-
// The native subagent deleteSession passes the bare key straight to
|
|
3292
|
-
// sessions.delete, which the 2026.6.x gateway indexes under the canonical
|
|
3293
|
-
// agent:<id>: form — the bare-key delete silently no-ops and the
|
|
3294
|
-
// excerpt-bearing transcript survives. deleteSessions() resolves the
|
|
3295
|
-
// canonical key via sessions.resolve first.
|
|
3630
|
+
|
|
3296
3631
|
deleteDistillerSession(sessionKey) {
|
|
3297
3632
|
return sessionService.deleteSessions("ocuclaw", [sessionKey]);
|
|
3298
3633
|
},
|
|
@@ -3313,11 +3648,6 @@ function createRelay(opts) {
|
|
|
3313
3648
|
return sessionService.peekSessionKey();
|
|
3314
3649
|
},
|
|
3315
3650
|
|
|
3316
|
-
/**
|
|
3317
|
-
* Test/shutdown hook: resolves once the async first-user-message cache
|
|
3318
|
-
* write has fully drained (no write in flight, no dirty mark pending) so
|
|
3319
|
-
* the on-disk file reflects the latest in-memory map.
|
|
3320
|
-
*/
|
|
3321
3651
|
flushFirstSentUserMessageCache() {
|
|
3322
3652
|
return sessionService.flushFirstSentUserMessageCache();
|
|
3323
3653
|
},
|
|
@@ -3334,20 +3664,10 @@ function createRelay(opts) {
|
|
|
3334
3664
|
return result;
|
|
3335
3665
|
},
|
|
3336
3666
|
|
|
3337
|
-
/**
|
|
3338
|
-
* Test-only: direct access to dispatchOcuClawUserSend so integration
|
|
3339
|
-
* tests can drive per-turn signal plumbing without a live downstream
|
|
3340
|
-
* WebSocket connection.
|
|
3341
|
-
*/
|
|
3342
3667
|
_dispatchOcuClawUserSend(params) {
|
|
3343
3668
|
return dispatchOcuClawUserSend(params || {});
|
|
3344
3669
|
},
|
|
3345
3670
|
|
|
3346
|
-
/**
|
|
3347
|
-
* Test-only: run the logical-reset state clear (the same call the /new,
|
|
3348
|
-
* /reset, and new-chat paths make) so integration tests can verify all
|
|
3349
|
-
* per-session state is dropped for a reused session key.
|
|
3350
|
-
*/
|
|
3351
3671
|
_clearLogicalSessionState(sessionKey) {
|
|
3352
3672
|
sessionService.clearLogicalSessionState(sessionKey);
|
|
3353
3673
|
},
|
|
@@ -3360,15 +3680,6 @@ function createRelay(opts) {
|
|
|
3360
3680
|
sendGlassesUiSurfaceUpdate(params);
|
|
3361
3681
|
},
|
|
3362
3682
|
|
|
3363
|
-
/**
|
|
3364
|
-
* Tap-to-wake lane (roadmap 6f): ONE agent turn for a parked glasses
|
|
3365
|
-
* gesture, dispatched through the same gateway client the voice send
|
|
3366
|
-
* uses. The MESSAGE is built (and sanitized) by the glasses-ui wake
|
|
3367
|
-
* controller — refs-only with non-wearer provenance framing; this method
|
|
3368
|
-
* is a dumb transport and deliberately does NOT touch the local
|
|
3369
|
-
* conversation state (a wake is not a wearer utterance — no synthetic
|
|
3370
|
-
* user message without provenance, §2.6).
|
|
3371
|
-
*/
|
|
3372
3683
|
dispatchGlassesWake(params) {
|
|
3373
3684
|
const sessionKey =
|
|
3374
3685
|
params && typeof params.sessionKey === "string" && params.sessionKey
|