ocuclaw 1.3.2 → 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 +93 -0
- 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 +657 -271
- package/dist/runtime/relay-service.js +40 -36
- 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 +109 -39
- package/dist/runtime/relay-worker-transport.js +157 -15
- 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 +58 -63
- 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 +22 -34
- package/dist/tools/glasses-ui-recipes.js +92 -101
- package/dist/tools/glasses-ui-surfaces.js +295 -100
- 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 +475 -331
- package/dist/tools/glasses-ui-voicemail.js +242 -0
- package/dist/tools/glasses-ui-wake.js +195 -0
- 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/skills/glasses-ui/SKILL.md +19 -3
- 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,23 @@ 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";
|
|
27
|
+
import { createAgentTurnTracker } from "../tools/glasses-ui-wake.js";
|
|
21
28
|
import { createDownstreamHandler } from "./downstream-handler.js";
|
|
22
|
-
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";
|
|
23
36
|
import { createRelayHealthMonitor } from "./relay-health-monitor.js";
|
|
37
|
+
import { createGlassesBackpressureLatch } from "./glasses-backpressure-latch.js";
|
|
24
38
|
import { createRelayOperationRegistry } from "./relay-operation-registry.js";
|
|
25
39
|
import { createRelayWorkerSupervisor } from "./relay-worker-supervisor.js";
|
|
26
40
|
import {
|
|
@@ -29,13 +43,18 @@ import {
|
|
|
29
43
|
} from "./session-service.js";
|
|
30
44
|
import { createUpstreamRuntime } from "./upstream-runtime.js";
|
|
31
45
|
|
|
46
|
+
const GLASSES_UI_MARKERS = new Set(["listening", "parked", "inflight"]);
|
|
47
|
+
export function sanitizeGlassesMarker(v) { return GLASSES_UI_MARKERS.has(v) ? v : undefined; }
|
|
48
|
+
|
|
32
49
|
const SONIOX_TEMP_KEY_URL = "https://api.soniox.com/v1/auth/temporary-api-key";
|
|
33
50
|
const SONIOX_MODELS_URL = "https://api.soniox.com/v1/models";
|
|
34
51
|
const DEFAULT_SONIOX_TEMP_KEY_EXPIRES_IN_SECONDS = 3600;
|
|
35
|
-
|
|
36
|
-
// fetch. 8 s is a conservative cold-path ceiling; tests inject a tiny value
|
|
37
|
-
// via opts.sonioxTemporaryKeyMintTimeoutMs to make assertions fast.
|
|
52
|
+
|
|
38
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;
|
|
39
58
|
const EVEN_AI_NAMESPACE_PREFIX = "ocuclaw:even-ai";
|
|
40
59
|
const EVEN_AI_NAMESPACE_PREFIX_WITH_DELIMITER = "ocuclaw:even-ai:";
|
|
41
60
|
const LISTEN_INTERCEPT_RECOVERY_ERROR = "Voice interrupted; retry";
|
|
@@ -174,7 +193,7 @@ function normalizeSonioxTemporaryKeyErrorCode(err) {
|
|
|
174
193
|
: "";
|
|
175
194
|
const lowered = message.toLowerCase();
|
|
176
195
|
if (!message) return "soniox_temp_key_request_failed";
|
|
177
|
-
|
|
196
|
+
|
|
178
197
|
if (err && err.name === "AbortError") {
|
|
179
198
|
return "soniox_temp_key_mint_timeout";
|
|
180
199
|
}
|
|
@@ -256,9 +275,7 @@ function createBufferedHttpResponse(maxResponseBytes) {
|
|
|
256
275
|
const limit = Number.isFinite(maxResponseBytes) && maxResponseBytes > 0
|
|
257
276
|
? Math.floor(maxResponseBytes)
|
|
258
277
|
: 262_144;
|
|
259
|
-
|
|
260
|
-
// res.once('close', ...) for client-disconnect detection. Worker-mode
|
|
261
|
-
// relays actual client closes through an http.cancel worker message.
|
|
278
|
+
|
|
262
279
|
const res = new EventEmitter();
|
|
263
280
|
res.statusCode = 200;
|
|
264
281
|
res.writableEnded = false;
|
|
@@ -296,29 +313,18 @@ function createBufferedHttpResponse(maxResponseBytes) {
|
|
|
296
313
|
return res;
|
|
297
314
|
}
|
|
298
315
|
|
|
299
|
-
// --- Factory ---
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Create the relay orchestrator.
|
|
303
|
-
*
|
|
304
|
-
* Wires the upstream gateway bridge to downstream clients via the
|
|
305
|
-
* conversation-state module, downstream handler, and downstream server.
|
|
306
|
-
* This is the only module that knows about both sides.
|
|
307
|
-
*
|
|
308
|
-
* @param {object} opts
|
|
309
|
-
* @param {number} opts.port - WebSocket server port
|
|
310
|
-
* @param {string} opts.host - WebSocket server bind address
|
|
311
|
-
* @param {string} opts.token - Authentication token for downstream clients
|
|
312
|
-
* @param {object} [opts.gatewayBridge] - Override bridge for testing/integration
|
|
313
|
-
* @param {object} [opts.openclawClient] - Override for testing (default: plugin gateway client)
|
|
314
|
-
* @param {object} [opts.conversationState] - Override for testing (default: require singleton)
|
|
315
|
-
* @param {object} [opts.logger] - Structured logger for shared runtime logs
|
|
316
|
-
* @param {string|null} [opts.consoleLogPath] - Optional shim-only browser console sink path
|
|
317
|
-
* @returns {object} Relay instance with start(), stop(), server
|
|
318
|
-
*/
|
|
319
316
|
function createRelay(opts) {
|
|
320
317
|
const logger = normalizeLogger(opts.logger);
|
|
321
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");
|
|
322
328
|
const openclawClient =
|
|
323
329
|
opts.openclawClient ||
|
|
324
330
|
(opts.gatewayBridge
|
|
@@ -342,30 +348,23 @@ function createRelay(opts) {
|
|
|
342
348
|
const activityStatusAdapter = createActivityStatusAdapter(
|
|
343
349
|
opts.activityStatusAdapter,
|
|
344
350
|
);
|
|
345
|
-
const sharedHttpServer = opts.httpServer || null;
|
|
346
351
|
|
|
347
|
-
|
|
352
|
+
const agentTurnTracker = createAgentTurnTracker();
|
|
353
|
+
const sharedHttpServer = opts.httpServer || null;
|
|
348
354
|
|
|
349
|
-
/** @type {string|null} Last formatted pages JSON string. */
|
|
350
355
|
let cachedPages = null;
|
|
351
|
-
|
|
356
|
+
|
|
352
357
|
let pagesRevision = 0;
|
|
353
358
|
|
|
354
|
-
/** @type {string|null} Last formatted status JSON string. */
|
|
355
359
|
let cachedStatus = null;
|
|
356
|
-
|
|
360
|
+
|
|
357
361
|
let statusRevision = 0;
|
|
358
|
-
|
|
362
|
+
|
|
359
363
|
let currentSessionModelConfigSnapshot = null;
|
|
360
364
|
|
|
361
|
-
/** Relay-local deterministic simulate-stream run sequence counter. */
|
|
362
365
|
let simulateStreamRunSeq = 0;
|
|
363
|
-
/** Active timers for relay-local deterministic simulate-stream runs. */
|
|
364
|
-
// timer -> sessionKey, so new-chat//reset/new-session can cancel ONLY the
|
|
365
|
-
// affected session's pending injections (re-land of a8a29032, session-scoped).
|
|
366
|
-
const simulateStreamTimers = new Map();
|
|
367
366
|
|
|
368
|
-
|
|
367
|
+
const simulateStreamTimers = new Map();
|
|
369
368
|
|
|
370
369
|
const debugCategories = Array.isArray(opts.debugCategories)
|
|
371
370
|
? opts.debugCategories
|
|
@@ -374,21 +373,10 @@ function createRelay(opts) {
|
|
|
374
373
|
.filter(([, enabled]) => enabled)
|
|
375
374
|
.map(([category]) => category)
|
|
376
375
|
: opts.debugCategories;
|
|
377
|
-
|
|
378
|
-
// liveui log tee all read from this one source so store records and `[liveui]`
|
|
379
|
-
// log lines share an identical ts (downstream reconcilers dedupe on it).
|
|
376
|
+
|
|
380
377
|
const debugNow =
|
|
381
378
|
typeof opts.debugNow === "function" ? opts.debugNow : () => Date.now();
|
|
382
379
|
|
|
383
|
-
// --- Durable debug-store arm (survives relay/gateway restarts) ---
|
|
384
|
-
// The capture arm (enabled categories + TTLs) lives only in the in-memory
|
|
385
|
-
// debug-store, which a restart rebuilds empty. We persist it to debug-arm.json
|
|
386
|
-
// (via persistDebugArm) and rehydrate it here at construction — read-once,
|
|
387
|
-
// mirroring liveUiTraceFlagPath below — so a restart no longer silently drops
|
|
388
|
-
// capture. This path covers process RESTART only: a pure WebUI *reload* does NOT
|
|
389
|
-
// restart the relay and already preserves + re-advertises the arm via
|
|
390
|
-
// relay-worker-transport.ts:327-328 (cache.debugConfig re-broadcast to app
|
|
391
|
-
// clients) — do not add reconnect machinery here.
|
|
392
380
|
const debugArmStatePath =
|
|
393
381
|
typeof opts.stateDir === "string" && opts.stateDir
|
|
394
382
|
? path.join(opts.stateDir, "debug-arm.json")
|
|
@@ -407,7 +395,6 @@ function createRelay(opts) {
|
|
|
407
395
|
const debugStore = createDebugStore({
|
|
408
396
|
categories: debugCategories,
|
|
409
397
|
capacity: opts.debugCapacity,
|
|
410
|
-
payloadMaxBytes: opts.debugPayloadMaxBytes,
|
|
411
398
|
defaultTtlMs: opts.debugDefaultTtlMs,
|
|
412
399
|
maxTtlMs: opts.debugMaxTtlMs,
|
|
413
400
|
dumpDefaultLimit: opts.debugDumpDefaultLimit,
|
|
@@ -417,10 +404,14 @@ function createRelay(opts) {
|
|
|
417
404
|
initialEnabled: initialDebugArm,
|
|
418
405
|
});
|
|
419
406
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
+
|
|
424
415
|
const liveUiTraceFlagPath =
|
|
425
416
|
typeof opts.stateDir === "string" && opts.stateDir
|
|
426
417
|
? path.join(opts.stateDir, "liveui-trace.json")
|
|
@@ -435,13 +426,11 @@ function createRelay(opts) {
|
|
|
435
426
|
}
|
|
436
427
|
}
|
|
437
428
|
|
|
438
|
-
// --- Console log file ---
|
|
439
|
-
|
|
440
429
|
const consoleLogPath =
|
|
441
430
|
typeof opts.consoleLogPath === "string" && opts.consoleLogPath.trim()
|
|
442
431
|
? opts.consoleLogPath
|
|
443
432
|
: null;
|
|
444
|
-
|
|
433
|
+
|
|
445
434
|
if (consoleLogPath) {
|
|
446
435
|
try {
|
|
447
436
|
fs.writeFileSync(consoleLogPath, "");
|
|
@@ -450,13 +439,6 @@ function createRelay(opts) {
|
|
|
450
439
|
const CONSOLE_LOG_MAX_LINES = 500;
|
|
451
440
|
const CONSOLE_LOG_TRIM_TO = 250;
|
|
452
441
|
|
|
453
|
-
/**
|
|
454
|
-
* Append a browser console message to the log file.
|
|
455
|
-
* Trims the file when it exceeds CONSOLE_LOG_MAX_LINES.
|
|
456
|
-
*
|
|
457
|
-
* @param {string} level - "log", "warn", or "error"
|
|
458
|
-
* @param {string} message - Console message text
|
|
459
|
-
*/
|
|
460
442
|
function writeConsoleLog(level, message) {
|
|
461
443
|
if (!consoleLogPath) {
|
|
462
444
|
logger.debug(`[browser:${level}] ${message}`);
|
|
@@ -466,7 +448,7 @@ function createRelay(opts) {
|
|
|
466
448
|
const line = `[${timestamp}] [${level}] ${message}\n`;
|
|
467
449
|
try {
|
|
468
450
|
fs.appendFileSync(consoleLogPath, line);
|
|
469
|
-
|
|
451
|
+
|
|
470
452
|
const content = fs.readFileSync(consoleLogPath, "utf8");
|
|
471
453
|
const lines = content.split("\n");
|
|
472
454
|
if (lines.length > CONSOLE_LOG_MAX_LINES) {
|
|
@@ -478,16 +460,6 @@ function createRelay(opts) {
|
|
|
478
460
|
}
|
|
479
461
|
}
|
|
480
462
|
|
|
481
|
-
/**
|
|
482
|
-
* Emit a structured debug event when a category is enabled.
|
|
483
|
-
* This keeps the disabled path cheap by avoiding payload construction.
|
|
484
|
-
*
|
|
485
|
-
* @param {string} cat
|
|
486
|
-
* @param {string} event
|
|
487
|
-
* @param {"debug"|"info"|"warn"|"error"} severity
|
|
488
|
-
* @param {object} context
|
|
489
|
-
* @param {() => object} buildData
|
|
490
|
-
*/
|
|
491
463
|
function emitDebug(cat, event, severity, context, buildData, options) {
|
|
492
464
|
const force = !!(options && options.force === true);
|
|
493
465
|
if (!force && !debugStore.isEnabled(cat) && !(liveUiTraceLogEnabled && (cat === "glasses.lifecycle" || cat === "openclaw.message"))) {
|
|
@@ -512,8 +484,6 @@ function createRelay(opts) {
|
|
|
512
484
|
|
|
513
485
|
debugStore.emit(payload, { force });
|
|
514
486
|
|
|
515
|
-
// Durable openclaw-side trace tee (gated by the persistent flag, NOT the
|
|
516
|
-
// store category enable). Must never throw into the emit path.
|
|
517
487
|
if (liveUiTraceLogEnabled && (cat === "glasses.lifecycle" || cat === "openclaw.message")) {
|
|
518
488
|
try {
|
|
519
489
|
const surfaceId =
|
|
@@ -541,7 +511,7 @@ function createRelay(opts) {
|
|
|
541
511
|
}),
|
|
542
512
|
);
|
|
543
513
|
} catch {
|
|
544
|
-
|
|
514
|
+
|
|
545
515
|
}
|
|
546
516
|
}
|
|
547
517
|
}
|
|
@@ -609,12 +579,26 @@ function createRelay(opts) {
|
|
|
609
579
|
)
|
|
610
580
|
? Math.max(1, Math.floor(opts.sonioxTemporaryKeyMintTimeoutMs))
|
|
611
581
|
: DEFAULT_SONIOX_TEMP_KEY_MINT_TIMEOUT_MS;
|
|
612
|
-
|
|
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
|
+
|
|
613
597
|
let cachedSonioxModels = null;
|
|
614
598
|
let cachedSonioxModelsFetchedAt = 0;
|
|
615
599
|
let cachedSonioxModelsStale = true;
|
|
616
600
|
let sonioxModelsFetchStarted = false;
|
|
617
|
-
|
|
601
|
+
|
|
618
602
|
let inFlightSonioxModelsFetch = null;
|
|
619
603
|
|
|
620
604
|
function resolveFetchImpl() {
|
|
@@ -926,6 +910,138 @@ function createRelay(opts) {
|
|
|
926
910
|
}
|
|
927
911
|
}
|
|
928
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
|
+
|
|
929
1045
|
let upstreamRuntime = null;
|
|
930
1046
|
const evenAiSettingsStore = createEvenAiSettingsStore({
|
|
931
1047
|
logger,
|
|
@@ -948,16 +1064,11 @@ function createRelay(opts) {
|
|
|
948
1064
|
stateDir: opts.stateDir,
|
|
949
1065
|
emitDebug,
|
|
950
1066
|
});
|
|
951
|
-
|
|
952
|
-
// cleared in stop(). Declared here so both can see it.
|
|
1067
|
+
|
|
953
1068
|
let stablePromptSweepTimer = null;
|
|
954
1069
|
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
// session's lifetime (see stable-prompt-snapshot). Mid-session toggles are
|
|
958
|
-
// bridged by the Channel-2 hook composer, NOT here. The glasses-UI pointer is
|
|
959
|
-
// always present (its disconnected gate moved to Channel 2); the emoji/pace
|
|
960
|
-
// blocks are included only when active at session start.
|
|
1070
|
+
let uploadCaptureArmingDisposer = null;
|
|
1071
|
+
|
|
961
1072
|
function computeStableChannelOne(startSignals) {
|
|
962
1073
|
const baseReadability = composeReadabilitySystemPrompt(
|
|
963
1074
|
ocuClawSettingsStore.getSnapshot().systemPrompt,
|
|
@@ -976,8 +1087,7 @@ function createRelay(opts) {
|
|
|
976
1087
|
|
|
977
1088
|
function stableSendOptions(resolvedSessionKey, sessionId, perTurnSignals) {
|
|
978
1089
|
const signals = perTurnSignals || {};
|
|
979
|
-
|
|
980
|
-
// session. The 3-state client signal maps to a boolean: only "active" counts.
|
|
1090
|
+
|
|
981
1091
|
const startEmoji = signals.neuralEmojiReactorState === "active";
|
|
982
1092
|
const startPace = signals.neuralPaceModulatorState === "active";
|
|
983
1093
|
const extraSystemPrompt = stablePromptSnapshots.getOrCreate(
|
|
@@ -985,10 +1095,7 @@ function createRelay(opts) {
|
|
|
985
1095
|
sessionId,
|
|
986
1096
|
() => computeStableChannelOne({ emoji: startEmoji, pace: startPace }),
|
|
987
1097
|
);
|
|
988
|
-
|
|
989
|
-
// something is mutating Channel 1 mid-session (e.g. a toggle flipped the
|
|
990
|
-
// start-state signals) — the case that used to reset the CLI session.
|
|
991
|
-
// Surface it loudly; the served prompt stays the frozen snapshot.
|
|
1098
|
+
|
|
992
1099
|
if (
|
|
993
1100
|
stablePromptSnapshots.wouldChurn(
|
|
994
1101
|
resolvedSessionKey,
|
|
@@ -1004,7 +1111,13 @@ function createRelay(opts) {
|
|
|
1004
1111
|
() => ({ sessionId: sessionId || null }),
|
|
1005
1112
|
);
|
|
1006
1113
|
}
|
|
1007
|
-
|
|
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;
|
|
1008
1121
|
}
|
|
1009
1122
|
|
|
1010
1123
|
function buildOcuClawSendDiagnostic(params = {}) {
|
|
@@ -1066,6 +1179,36 @@ function createRelay(opts) {
|
|
|
1066
1179
|
return userContent;
|
|
1067
1180
|
}
|
|
1068
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
|
+
|
|
1069
1212
|
function buildOcuClawInitialSessionConfigPatch(settings) {
|
|
1070
1213
|
const patch = {};
|
|
1071
1214
|
if (settings && typeof settings.defaultModel === "string" && settings.defaultModel.trim()) {
|
|
@@ -1084,6 +1227,30 @@ function createRelay(opts) {
|
|
|
1084
1227
|
return Object.keys(patch).length > 0 ? patch : null;
|
|
1085
1228
|
}
|
|
1086
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
|
+
|
|
1087
1254
|
async function maybeSeedOcuClawSessionConfig(sessionKey) {
|
|
1088
1255
|
if (
|
|
1089
1256
|
!sessionKey ||
|
|
@@ -1095,6 +1262,7 @@ function createRelay(opts) {
|
|
|
1095
1262
|
}
|
|
1096
1263
|
|
|
1097
1264
|
const settings = ocuClawSettingsStore.getSnapshot();
|
|
1265
|
+
seedSessionAgentDefault(sessionKey, settings.defaultAgent);
|
|
1098
1266
|
const patch = buildOcuClawInitialSessionConfigPatch(settings);
|
|
1099
1267
|
if (!patch) {
|
|
1100
1268
|
sessionService.clearPendingInitialConfig(sessionKey);
|
|
@@ -1122,6 +1290,7 @@ function createRelay(opts) {
|
|
|
1122
1290
|
}
|
|
1123
1291
|
|
|
1124
1292
|
const settings = ocuClawSettingsStore.getSnapshot();
|
|
1293
|
+
seedSessionAgentDefault(sessionKey, settings.defaultAgent);
|
|
1125
1294
|
const patch = buildOcuClawInitialSessionConfigPatch(settings);
|
|
1126
1295
|
if (!patch) {
|
|
1127
1296
|
return null;
|
|
@@ -1154,6 +1323,17 @@ function createRelay(opts) {
|
|
|
1154
1323
|
getAgentName() {
|
|
1155
1324
|
return upstreamRuntime ? upstreamRuntime.getAgentName() : null;
|
|
1156
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
|
+
},
|
|
1157
1337
|
isPinnedFirstUserMessageKey(sessionKey) {
|
|
1158
1338
|
const normalizedSessionKey = normalizeEvenAiSessionKeyForLookup(sessionKey);
|
|
1159
1339
|
if (!normalizedSessionKey) {
|
|
@@ -1247,13 +1427,6 @@ function createRelay(opts) {
|
|
|
1247
1427
|
}
|
|
1248
1428
|
}
|
|
1249
1429
|
|
|
1250
|
-
// TTL fallback for set_session_title activity label. The tool itself
|
|
1251
|
-
// completes in <50ms but its label can linger if no follow-up activity
|
|
1252
|
-
// arrives (e.g. agent streams a response directly after, with no
|
|
1253
|
-
// intervening activity event). After 1s, synthesize a thinking-status
|
|
1254
|
-
// activity with no tool/label so the renderer falls back to the bare
|
|
1255
|
-
// animated spinner. Any real activity arriving in the meantime cancels
|
|
1256
|
-
// the timer.
|
|
1257
1430
|
const SESSION_TITLE_STATUS_FALLBACK_MS = 1500;
|
|
1258
1431
|
let sessionTitleStatusFallbackTimer = null;
|
|
1259
1432
|
|
|
@@ -1262,6 +1435,10 @@ function createRelay(opts) {
|
|
|
1262
1435
|
const runId = activity && activity.runId ? activity.runId : null;
|
|
1263
1436
|
const origin = activity && activity.origin ? activity.origin : null;
|
|
1264
1437
|
const phase = activity && activity.phase ? activity.phase : null;
|
|
1438
|
+
agentTurnTracker.onActivity(
|
|
1439
|
+
(activity && activity.sessionKey) || sessionService.ensureSessionKey(),
|
|
1440
|
+
phase,
|
|
1441
|
+
);
|
|
1265
1442
|
|
|
1266
1443
|
emitDebug(
|
|
1267
1444
|
"app.timeline",
|
|
@@ -1324,6 +1501,20 @@ function createRelay(opts) {
|
|
|
1324
1501
|
return snapshot;
|
|
1325
1502
|
}
|
|
1326
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
|
+
|
|
1327
1518
|
const appClientDisconnectHandlers = new Set();
|
|
1328
1519
|
function onAppClientDisconnect(handler) {
|
|
1329
1520
|
if (typeof handler !== "function") return () => {};
|
|
@@ -1348,6 +1539,7 @@ function createRelay(opts) {
|
|
|
1348
1539
|
surfaceId: params && typeof params.surfaceId === "string" ? params.surfaceId : "",
|
|
1349
1540
|
depth: Number.isFinite(params && params.depth) ? Math.floor(params.depth) : 1,
|
|
1350
1541
|
spec: params && params.spec ? params.spec : null,
|
|
1542
|
+
marker: sanitizeGlassesMarker(params && params.marker),
|
|
1351
1543
|
};
|
|
1352
1544
|
server.broadcast(JSON.stringify(payload));
|
|
1353
1545
|
emitDebug(
|
|
@@ -1367,9 +1559,7 @@ function createRelay(opts) {
|
|
|
1367
1559
|
if (typeof patch.title === "string") cleanPatch.title = patch.title;
|
|
1368
1560
|
if (typeof patch.body === "string") cleanPatch.body = patch.body;
|
|
1369
1561
|
if (Array.isArray(patch.items)) {
|
|
1370
|
-
|
|
1371
|
-
// {label, body} objects (list_with_details detail-body ticks). Keep both
|
|
1372
|
-
// shapes; drop anything malformed (no string, no string label).
|
|
1562
|
+
|
|
1373
1563
|
cleanPatch.items = patch.items
|
|
1374
1564
|
.map((i) => {
|
|
1375
1565
|
if (typeof i === "string") return i;
|
|
@@ -1382,6 +1572,7 @@ function createRelay(opts) {
|
|
|
1382
1572
|
})
|
|
1383
1573
|
.filter((i) => i !== null);
|
|
1384
1574
|
}
|
|
1575
|
+
const m = sanitizeGlassesMarker(patch.marker); if (m) cleanPatch.marker = m;
|
|
1385
1576
|
const payload = {
|
|
1386
1577
|
type: "glasses_ui_surface_update",
|
|
1387
1578
|
sessionKey: params && typeof params.sessionKey === "string" ? params.sessionKey : null,
|
|
@@ -1542,8 +1733,9 @@ function createRelay(opts) {
|
|
|
1542
1733
|
);
|
|
1543
1734
|
|
|
1544
1735
|
return maybeSeedOcuClawSessionConfig(resolvedSessionKey).then(() => {
|
|
1545
|
-
|
|
1546
|
-
|
|
1736
|
+
|
|
1737
|
+
agentTurnTracker.markBusy(resolvedSessionKey);
|
|
1738
|
+
|
|
1547
1739
|
const upstreamPromise = gatewayBridge.sendMessage(
|
|
1548
1740
|
text,
|
|
1549
1741
|
resolvedSessionKey,
|
|
@@ -1551,11 +1743,7 @@ function createRelay(opts) {
|
|
|
1551
1743
|
{
|
|
1552
1744
|
...stableSendOptions(
|
|
1553
1745
|
resolvedSessionKey,
|
|
1554
|
-
|
|
1555
|
-
// (resolveSessionCanonicalKey is async). Use the sessionKey as the
|
|
1556
|
-
// snapshot's id; the sessionId-mismatch guard is therefore a no-op,
|
|
1557
|
-
// and new-session safety rests on logical-session-end eviction
|
|
1558
|
-
// (onNewSession / onNewChat / onDeleteSessions evict the snapshot).
|
|
1746
|
+
|
|
1559
1747
|
resolvedSessionKey,
|
|
1560
1748
|
clientDisplaySignals,
|
|
1561
1749
|
),
|
|
@@ -1657,6 +1845,107 @@ function createRelay(opts) {
|
|
|
1657
1845
|
});
|
|
1658
1846
|
}
|
|
1659
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
|
+
|
|
1660
1949
|
function emitListenInterceptRecovery(params = {}) {
|
|
1661
1950
|
const connectedAppClients = server ? server.getConnectedAppCount() : 0;
|
|
1662
1951
|
if (!server || !handler) {
|
|
@@ -1687,9 +1976,6 @@ function createRelay(opts) {
|
|
|
1687
1976
|
server.broadcast(handler.formatEvenAiListenIntercepted(sessionKey));
|
|
1688
1977
|
}
|
|
1689
1978
|
|
|
1690
|
-
// --- Downstream handler ---
|
|
1691
|
-
|
|
1692
|
-
/** @type {ReturnType<typeof createRelayWorkerSupervisor>|null} */
|
|
1693
1979
|
let server = null;
|
|
1694
1980
|
let evenAiEndpoint = null;
|
|
1695
1981
|
let evenAiRouter = null;
|
|
@@ -1713,12 +1999,6 @@ function createRelay(opts) {
|
|
|
1713
1999
|
return { ok: true, enabled, persisted, persistedPath: liveUiTraceFlagPath };
|
|
1714
2000
|
}
|
|
1715
2001
|
|
|
1716
|
-
// Persist the current debug-store arm to debug-arm.json. Mirrors the
|
|
1717
|
-
// applyTraceLogSet writeFileSync above (plain, non-atomic): a partial/corrupt
|
|
1718
|
-
// write degrades to an empty arm on next boot — acceptable, the nothing-armed
|
|
1719
|
-
// warning catches it. getSnapshot().enabled is already pruned of expired
|
|
1720
|
-
// categories, so the persisted JSON never holds an expired entry. Never throws
|
|
1721
|
-
// into the caller.
|
|
1722
2002
|
function persistDebugArm() {
|
|
1723
2003
|
if (!debugArmStatePath) return false;
|
|
1724
2004
|
try {
|
|
@@ -1736,9 +2016,7 @@ function createRelay(opts) {
|
|
|
1736
2016
|
if (!result.ok) {
|
|
1737
2017
|
throw new Error(result.error || "debug-set failed");
|
|
1738
2018
|
}
|
|
1739
|
-
|
|
1740
|
-
// on-disk arm always tracks live state and a deliberately-cleared arm is not
|
|
1741
|
-
// resurrected on the next restart.
|
|
2019
|
+
|
|
1742
2020
|
persistDebugArm();
|
|
1743
2021
|
emitDebug(
|
|
1744
2022
|
"relay.protocol",
|
|
@@ -1764,15 +2042,7 @@ function createRelay(opts) {
|
|
|
1764
2042
|
if (kind === "status") return statusRevision;
|
|
1765
2043
|
return null;
|
|
1766
2044
|
},
|
|
1767
|
-
|
|
1768
|
-
* Forward a user message to the upstream OpenClaw agent.
|
|
1769
|
-
*
|
|
1770
|
-
* @param {string} id - Message ID
|
|
1771
|
-
* @param {string} text - User message text
|
|
1772
|
-
* @param {string|null} sessionKey - Session key
|
|
1773
|
-
* @param {object|null} attachment - Optional image attachment payload
|
|
1774
|
-
* @returns {Promise}
|
|
1775
|
-
*/
|
|
2045
|
+
|
|
1776
2046
|
onSend(id, text, sessionKey, attachment, clientDisplaySignals) {
|
|
1777
2047
|
return dispatchOcuClawUserSend({
|
|
1778
2048
|
id,
|
|
@@ -1783,6 +2053,17 @@ function createRelay(opts) {
|
|
|
1783
2053
|
source: "phone_ui",
|
|
1784
2054
|
});
|
|
1785
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
|
+
},
|
|
1786
2067
|
onGlassesUiResult(frame) {
|
|
1787
2068
|
emitDebug(
|
|
1788
2069
|
"glasses.lifecycle",
|
|
@@ -1873,19 +2154,89 @@ function createRelay(opts) {
|
|
|
1873
2154
|
}
|
|
1874
2155
|
});
|
|
1875
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
|
+
},
|
|
1876
2238
|
operationRegistry: relayOperationRegistry,
|
|
1877
2239
|
|
|
1878
|
-
/**
|
|
1879
|
-
* Inject a fake assistant message into conversation state.
|
|
1880
|
-
*
|
|
1881
|
-
* The sender name is used as a temporary agent name prefix for
|
|
1882
|
-
* this message. If no agent identity has been established yet,
|
|
1883
|
-
* the sender becomes the default agent name going forward.
|
|
1884
|
-
*
|
|
1885
|
-
* @param {string} sender - Display name for the simulated message
|
|
1886
|
-
* @param {string} text - Message text
|
|
1887
|
-
* @returns {Array<{content: string, subPage: [number, number]|null}>}
|
|
1888
|
-
*/
|
|
1889
2240
|
onSimulate(sender, text) {
|
|
1890
2241
|
emitDebug(
|
|
1891
2242
|
"relay.protocol",
|
|
@@ -1897,7 +2248,7 @@ function createRelay(opts) {
|
|
|
1897
2248
|
textChars: typeof text === "string" ? text.length : 0,
|
|
1898
2249
|
}),
|
|
1899
2250
|
);
|
|
1900
|
-
|
|
2251
|
+
|
|
1901
2252
|
conversationState.addMessage("assistant", [{ type: "text", text }], sender || "Simulator");
|
|
1902
2253
|
|
|
1903
2254
|
const pages = conversationState.getPages();
|
|
@@ -2015,12 +2366,6 @@ function createRelay(opts) {
|
|
|
2015
2366
|
});
|
|
2016
2367
|
},
|
|
2017
2368
|
|
|
2018
|
-
/**
|
|
2019
|
-
* Clear conversation state, reset cached pages, and send /new to OpenClaw.
|
|
2020
|
-
* Legacy support: delegates to newSession.
|
|
2021
|
-
*
|
|
2022
|
-
* @returns {Promise<Array>} Empty pages array
|
|
2023
|
-
*/
|
|
2024
2369
|
onNewChat() {
|
|
2025
2370
|
emitDebug(
|
|
2026
2371
|
"relay.session",
|
|
@@ -2034,15 +2379,10 @@ function createRelay(opts) {
|
|
|
2034
2379
|
}
|
|
2035
2380
|
sessionService.invalidateSessionsCache();
|
|
2036
2381
|
resetActivityStatusAdapter();
|
|
2037
|
-
|
|
2038
|
-
// a deferred addMessage firing after the clear repopulates the fresh chat
|
|
2039
|
-
// (the 2026-05-15 canary-pollution mechanism).
|
|
2382
|
+
|
|
2040
2383
|
clearSimulateStreamTimersForSession(sessionService.ensureSessionKey());
|
|
2041
2384
|
conversationState.clear();
|
|
2042
|
-
|
|
2043
|
-
// Channel-1 snapshot must be dropped or the next send serves a stale prompt,
|
|
2044
|
-
// and ALL other per-session state keyed to it (title + upstream name,
|
|
2045
|
-
// toggles, distiller budget, first-user marker) must be cleared too.
|
|
2385
|
+
|
|
2046
2386
|
const newChatSessionKey = sessionService.ensureSessionKey();
|
|
2047
2387
|
stablePromptSnapshots.evict(newChatSessionKey);
|
|
2048
2388
|
sessionService.clearLogicalSessionState(newChatSessionKey);
|
|
@@ -2052,12 +2392,7 @@ function createRelay(opts) {
|
|
|
2052
2392
|
const pages = conversationState.getPages();
|
|
2053
2393
|
cachePages(pages);
|
|
2054
2394
|
if (upstreamRuntime && upstreamRuntime.isConnected()) {
|
|
2055
|
-
|
|
2056
|
-
// changing currentSessionKey, so it must NOT elicit a welcome turn here:
|
|
2057
|
-
// the turn's events would carry "main" and be dropped by isCurrentSession()
|
|
2058
|
-
// whenever the active session is an ocuclaw:* key. The welcome restore for
|
|
2059
|
-
// the real glasses paths lives in newSession() (New) and onSlashCommand
|
|
2060
|
-
// "/reset" (Reset). Unifying onNewChat onto newSession() is Phase-2 work.
|
|
2395
|
+
|
|
2061
2396
|
gatewayBridge.sendMessage("/new", "main").catch((err) => {
|
|
2062
2397
|
logger.error(`[relay] Failed to send /new: ${err.message}`);
|
|
2063
2398
|
});
|
|
@@ -2083,16 +2418,10 @@ function createRelay(opts) {
|
|
|
2083
2418
|
},
|
|
2084
2419
|
|
|
2085
2420
|
async onNewSession() {
|
|
2086
|
-
|
|
2087
|
-
// BEFORE the new key is minted — a deferred addMessage firing after the
|
|
2088
|
-
// switch would repopulate the fresh session's shared conversation view.
|
|
2421
|
+
|
|
2089
2422
|
clearSimulateStreamTimersForSession(sessionService.ensureSessionKey());
|
|
2090
2423
|
const result = await sessionService.newSession();
|
|
2091
|
-
|
|
2092
|
-
// has no snapshot yet). Do NOT touch the previous key: that session stays
|
|
2093
|
-
// resumable via onSwitchSession, and dropping its frozen snapshot would
|
|
2094
|
-
// recompute — and churn — Channel 1 if the user switches back to it. The
|
|
2095
|
-
// previous session's snapshot is released by delete or the TTL sweep.
|
|
2424
|
+
|
|
2096
2425
|
if (result && typeof result.sessionKey === "string" && result.sessionKey.trim()) {
|
|
2097
2426
|
stablePromptSnapshots.evict(result.sessionKey);
|
|
2098
2427
|
sessionService.clearDisplayToggleStates(result.sessionKey);
|
|
@@ -2127,6 +2456,20 @@ function createRelay(opts) {
|
|
|
2127
2456
|
: Promise.resolve({ skills: [], fetchedAtMs: Date.now(), stale: true });
|
|
2128
2457
|
},
|
|
2129
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
|
+
|
|
2130
2473
|
onGetProviderUsageSnapshot() {
|
|
2131
2474
|
return upstreamRuntime
|
|
2132
2475
|
? upstreamRuntime.getProviderUsageSnapshot()
|
|
@@ -2167,6 +2510,32 @@ function createRelay(opts) {
|
|
|
2167
2510
|
return result;
|
|
2168
2511
|
},
|
|
2169
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
|
+
|
|
2170
2539
|
onGetEvenAiSettings() {
|
|
2171
2540
|
return evenAiSettingsStore.getSnapshot();
|
|
2172
2541
|
},
|
|
@@ -2216,23 +2585,15 @@ function createRelay(opts) {
|
|
|
2216
2585
|
);
|
|
2217
2586
|
broadcastPages();
|
|
2218
2587
|
}
|
|
2219
|
-
|
|
2220
|
-
// key (distinct from an automatic CLI session reset, which keeps the same
|
|
2221
|
-
// logical session and must survive). Drop the frozen Channel-1 snapshot so
|
|
2222
|
-
// the next real message recomputes it for the fresh conversation; otherwise
|
|
2223
|
-
// the old conversation's prompt + display start-state bleed into the new one.
|
|
2588
|
+
|
|
2224
2589
|
if (command === "/new" || command === "/reset") {
|
|
2225
2590
|
const resetKey = sessionService.ensureSessionKey();
|
|
2226
2591
|
stablePromptSnapshots.evict(resetKey);
|
|
2227
|
-
|
|
2228
|
-
// name, toggles, distiller budget, first-user marker) so nothing from the
|
|
2229
|
-
// old conversation bleeds into the fresh one.
|
|
2592
|
+
|
|
2230
2593
|
sessionService.clearLogicalSessionState(resetKey);
|
|
2231
2594
|
}
|
|
2232
2595
|
if (upstreamRuntime && upstreamRuntime.isConnected()) {
|
|
2233
|
-
|
|
2234
|
-
// (fast-reset). Append the greeting prompt so Reset gets the same
|
|
2235
|
-
// welcome as New. Other slash commands forward verbatim.
|
|
2596
|
+
|
|
2236
2597
|
const outboundCommand =
|
|
2237
2598
|
command === "/reset"
|
|
2238
2599
|
? `/reset ${NEW_SESSION_GREETING_PROMPT}`
|
|
@@ -2245,9 +2606,6 @@ function createRelay(opts) {
|
|
|
2245
2606
|
return Promise.resolve();
|
|
2246
2607
|
},
|
|
2247
2608
|
|
|
2248
|
-
/**
|
|
2249
|
-
* @returns {boolean} Whether upstream is connected.
|
|
2250
|
-
*/
|
|
2251
2609
|
isUpstreamConnected() {
|
|
2252
2610
|
return true;
|
|
2253
2611
|
},
|
|
@@ -2298,6 +2656,10 @@ function createRelay(opts) {
|
|
|
2298
2656
|
return mintSonioxTemporaryKey(clientId, request);
|
|
2299
2657
|
},
|
|
2300
2658
|
|
|
2659
|
+
onRequestCartesiaAccessToken(clientId, request) {
|
|
2660
|
+
return mintCartesiaAccessToken(clientId, request);
|
|
2661
|
+
},
|
|
2662
|
+
|
|
2301
2663
|
onDebugSet(clientId, request) {
|
|
2302
2664
|
return applyDebugSet(clientId, request);
|
|
2303
2665
|
},
|
|
@@ -2545,14 +2907,7 @@ function createRelay(opts) {
|
|
|
2545
2907
|
},
|
|
2546
2908
|
|
|
2547
2909
|
onAutomationState(clientId, request) {
|
|
2548
|
-
|
|
2549
|
-
// client via the readiness snapshot, then return a dispatch envelope
|
|
2550
|
-
// that downstream-handler.handleAutomationState wraps into
|
|
2551
|
-
// `automationStateRequest`. Without this callback wired, the handler
|
|
2552
|
-
// returns null and the request is silently dropped at the relay —
|
|
2553
|
-
// simctl/debugctl times out with no failure response, no trace event,
|
|
2554
|
-
// no outbox drop. The lack of wiring was found 2026-05-28 while
|
|
2555
|
-
// validating the streaming-thinking-emoji-demotion fix on the sim.
|
|
2910
|
+
|
|
2556
2911
|
const now = Date.now();
|
|
2557
2912
|
const requestId =
|
|
2558
2913
|
(typeof request.requestId === "string" && request.requestId.trim()) ||
|
|
@@ -2581,11 +2936,7 @@ function createRelay(opts) {
|
|
|
2581
2936
|
targetEntry && typeof targetEntry.clientId === "string"
|
|
2582
2937
|
? targetEntry.clientId
|
|
2583
2938
|
: null;
|
|
2584
|
-
|
|
2585
|
-
// cannot answer an automation state request; forwarding anyway would
|
|
2586
|
-
// park the request in pendingAutomationStateRequests with no reply.
|
|
2587
|
-
// Same predicate as the downstream readiness gate; this wired callback
|
|
2588
|
-
// bypasses the normal dispatch path.
|
|
2939
|
+
|
|
2589
2940
|
const readinessPublished =
|
|
2590
2941
|
!!(
|
|
2591
2942
|
targetEntry &&
|
|
@@ -2672,10 +3023,13 @@ function createRelay(opts) {
|
|
|
2672
3023
|
},
|
|
2673
3024
|
});
|
|
2674
3025
|
|
|
2675
|
-
// --- Worker supervisor ---
|
|
2676
|
-
|
|
2677
3026
|
const pluginVersionService = createPluginVersionService();
|
|
2678
3027
|
|
|
3028
|
+
const glassesBackpressureLatch = createGlassesBackpressureLatch({
|
|
3029
|
+
emitDebug: (event, severity, data) =>
|
|
3030
|
+
emitDebug("relay.health", event, severity, null, () => data || {}),
|
|
3031
|
+
});
|
|
3032
|
+
|
|
2679
3033
|
server = createRelayWorkerSupervisor({
|
|
2680
3034
|
pluginId: "ocuclaw",
|
|
2681
3035
|
getPluginVersion: () => pluginVersionService.getPluginVersion(),
|
|
@@ -2686,6 +3040,7 @@ function createRelay(opts) {
|
|
|
2686
3040
|
host: opts.host,
|
|
2687
3041
|
port: opts.port,
|
|
2688
3042
|
token: opts.token,
|
|
3043
|
+
onWorkerBackpressure: (message) => glassesBackpressureLatch.report(message),
|
|
2689
3044
|
externalDebugToolsEnabled,
|
|
2690
3045
|
evenAiRequestTimeoutMs: opts.evenAiRequestTimeoutMs,
|
|
2691
3046
|
evenAiMaxBodyBytes: opts.evenAiMaxBodyBytes,
|
|
@@ -2730,8 +3085,6 @@ function createRelay(opts) {
|
|
|
2730
3085
|
},
|
|
2731
3086
|
});
|
|
2732
3087
|
|
|
2733
|
-
// --- Helpers ---
|
|
2734
|
-
|
|
2735
3088
|
function buildStatusObject(options = {}) {
|
|
2736
3089
|
const includeDownstreamReadiness = options.includeDownstreamReadiness === true;
|
|
2737
3090
|
const status = {
|
|
@@ -2779,9 +3132,6 @@ function createRelay(opts) {
|
|
|
2779
3132
|
return cachedStatus;
|
|
2780
3133
|
}
|
|
2781
3134
|
|
|
2782
|
-
/**
|
|
2783
|
-
* Recompute pages from conversation state, cache, and broadcast.
|
|
2784
|
-
*/
|
|
2785
3135
|
function broadcastPages() {
|
|
2786
3136
|
const pages = conversationState.getPages();
|
|
2787
3137
|
const next = cachePages(pages);
|
|
@@ -2790,12 +3140,6 @@ function createRelay(opts) {
|
|
|
2790
3140
|
}
|
|
2791
3141
|
}
|
|
2792
3142
|
|
|
2793
|
-
/**
|
|
2794
|
-
* Fetch the latest sessions snapshot and broadcast it. Used after a session
|
|
2795
|
-
* title changes so connected clients refresh the title in the main webui
|
|
2796
|
-
* status row and Session Settings tab without waiting for a manual
|
|
2797
|
-
* session-list open.
|
|
2798
|
-
*/
|
|
2799
3143
|
function broadcastSessions() {
|
|
2800
3144
|
sessionService
|
|
2801
3145
|
.getSessions()
|
|
@@ -2813,10 +3157,6 @@ function createRelay(opts) {
|
|
|
2813
3157
|
});
|
|
2814
3158
|
}
|
|
2815
3159
|
|
|
2816
|
-
/**
|
|
2817
|
-
* Resolve the current Even AI sessions snapshot for unicast/broadcast.
|
|
2818
|
-
* Mirrors the shape that `formatEvenAiSessions` expects.
|
|
2819
|
-
*/
|
|
2820
3160
|
async function buildEvenAiSessionsSnapshot() {
|
|
2821
3161
|
const dedicatedKey =
|
|
2822
3162
|
evenAiRouter && typeof evenAiRouter.getDedicatedSessionKey === "function"
|
|
@@ -2881,9 +3221,6 @@ function createRelay(opts) {
|
|
|
2881
3221
|
});
|
|
2882
3222
|
}
|
|
2883
3223
|
|
|
2884
|
-
/**
|
|
2885
|
-
* Build, cache, and broadcast the current status.
|
|
2886
|
-
*/
|
|
2887
3224
|
function broadcastStatus() {
|
|
2888
3225
|
const next = cacheStatus(buildStatusObject());
|
|
2889
3226
|
if (next !== null) {
|
|
@@ -2916,6 +3253,7 @@ function createRelay(opts) {
|
|
|
2916
3253
|
broadcastStatus,
|
|
2917
3254
|
broadcastActivity,
|
|
2918
3255
|
broadcastProviderUsageSnapshot,
|
|
3256
|
+
broadcastAgentsCatalog,
|
|
2919
3257
|
operationRegistry: relayOperationRegistry,
|
|
2920
3258
|
getCurrentSessionModelConfigSnapshot() {
|
|
2921
3259
|
return currentSessionModelConfigSnapshot;
|
|
@@ -2933,10 +3271,6 @@ function createRelay(opts) {
|
|
|
2933
3271
|
fetchAgentAvatar: opts.fetchAgentAvatar,
|
|
2934
3272
|
});
|
|
2935
3273
|
|
|
2936
|
-
// Shared routing gate for session-scoped Even AI defaults (thinking seed,
|
|
2937
|
-
// fast-mode patch): never touch active-routed sessions; always seed fresh
|
|
2938
|
-
// background_new sessions; seed persistent background sessions only before
|
|
2939
|
-
// their first turn exists.
|
|
2940
3274
|
async function shouldSeedSessionScopedDefaultForRoute(route) {
|
|
2941
3275
|
const routingMode =
|
|
2942
3276
|
route && typeof route.routingMode === "string"
|
|
@@ -3045,6 +3379,29 @@ function createRelay(opts) {
|
|
|
3045
3379
|
);
|
|
3046
3380
|
return !!(result && result.status === "accepted");
|
|
3047
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
|
+
},
|
|
3048
3405
|
onSessionActivated(route) {
|
|
3049
3406
|
if (!route || !route.sessionChanged) {
|
|
3050
3407
|
return;
|
|
@@ -3105,34 +3462,45 @@ function createRelay(opts) {
|
|
|
3105
3462
|
return true;
|
|
3106
3463
|
}
|
|
3107
3464
|
|
|
3108
|
-
// --- Public API ---
|
|
3109
|
-
|
|
3110
3465
|
relayApi = {
|
|
3111
|
-
|
|
3112
|
-
* Emit a glasses-UI surface-lifecycle event on the permanent
|
|
3113
|
-
* `glasses.lifecycle` debug category (nav reconcile + cron pause/resume/
|
|
3114
|
-
* tick). Recorded only when the category is enabled via debug-set. Wired
|
|
3115
|
-
* through the relay-service facade into the glasses-ui tool handler + cron
|
|
3116
|
-
* engine. See docs/superpowers/findings/2026-05-30-glasses-ui-phase4-hardware.md.
|
|
3117
|
-
*/
|
|
3466
|
+
|
|
3118
3467
|
emitGlassesUiLifecycle(event, severity, data) {
|
|
3119
3468
|
emitDebug("glasses.lifecycle", event, severity, {}, () => data || {});
|
|
3120
3469
|
},
|
|
3121
|
-
|
|
3122
|
-
* Start the upstream OpenClaw connection.
|
|
3123
|
-
* The downstream server is already listening from construction.
|
|
3124
|
-
*/
|
|
3470
|
+
|
|
3125
3471
|
start() {
|
|
3126
|
-
|
|
3472
|
+
|
|
3473
|
+
if (!bundleCacheSweepTimer) {
|
|
3474
|
+
bundleCacheSweepTimer = setInterval(() => bundleCache.sweep(), 60_000);
|
|
3475
|
+
if (typeof bundleCacheSweepTimer.unref === "function") bundleCacheSweepTimer.unref();
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3127
3478
|
if (!stablePromptSweepTimer) {
|
|
3128
3479
|
stablePromptSweepTimer = setInterval(
|
|
3129
3480
|
() => stablePromptSnapshots.sweep(),
|
|
3130
|
-
60 * 60 * 1000,
|
|
3481
|
+
60 * 60 * 1000,
|
|
3131
3482
|
);
|
|
3132
3483
|
if (typeof stablePromptSweepTimer.unref === "function") {
|
|
3133
3484
|
stablePromptSweepTimer.unref();
|
|
3134
3485
|
}
|
|
3135
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
|
+
}
|
|
3136
3504
|
const startGateway = () => Promise.resolve(gatewayBridge.start()).then(() => {
|
|
3137
3505
|
prefetchSonioxModels("relay_start").catch((err) => {
|
|
3138
3506
|
logger.warn(`[relay] Soniox models prefetch failed: ${err.message}`);
|
|
@@ -3147,17 +3515,20 @@ function createRelay(opts) {
|
|
|
3147
3515
|
return startGateway();
|
|
3148
3516
|
},
|
|
3149
3517
|
|
|
3150
|
-
/**
|
|
3151
|
-
* Stop the upstream connection and shut down the downstream server.
|
|
3152
|
-
*
|
|
3153
|
-
* @returns {Promise<void>}
|
|
3154
|
-
*/
|
|
3155
3518
|
stop() {
|
|
3156
3519
|
clearSimulateStreamTimers();
|
|
3520
|
+
if (bundleCacheSweepTimer) {
|
|
3521
|
+
clearInterval(bundleCacheSweepTimer);
|
|
3522
|
+
bundleCacheSweepTimer = null;
|
|
3523
|
+
}
|
|
3157
3524
|
if (stablePromptSweepTimer) {
|
|
3158
3525
|
clearInterval(stablePromptSweepTimer);
|
|
3159
3526
|
stablePromptSweepTimer = null;
|
|
3160
3527
|
}
|
|
3528
|
+
if (uploadCaptureArmingDisposer) {
|
|
3529
|
+
uploadCaptureArmingDisposer();
|
|
3530
|
+
uploadCaptureArmingDisposer = null;
|
|
3531
|
+
}
|
|
3161
3532
|
if (evenAiEndpoint) {
|
|
3162
3533
|
evenAiEndpoint.close();
|
|
3163
3534
|
}
|
|
@@ -3184,7 +3555,6 @@ function createRelay(opts) {
|
|
|
3184
3555
|
|
|
3185
3556
|
handleBufferedEvenAiHttpRequest,
|
|
3186
3557
|
|
|
3187
|
-
/** The downstream server instance. */
|
|
3188
3558
|
get server() {
|
|
3189
3559
|
return server;
|
|
3190
3560
|
},
|
|
@@ -3245,7 +3615,6 @@ function createRelay(opts) {
|
|
|
3245
3615
|
return sessionService.getDisplayCurrentStates(sessionKey);
|
|
3246
3616
|
},
|
|
3247
3617
|
|
|
3248
|
-
// Accessors used by the session-title distiller sidecar.
|
|
3249
3618
|
getSessionTitleRecord(sessionKey) {
|
|
3250
3619
|
return sessionService.getSessionTitleRecord(sessionKey);
|
|
3251
3620
|
},
|
|
@@ -3258,12 +3627,7 @@ function createRelay(opts) {
|
|
|
3258
3627
|
getDistillerBudget() {
|
|
3259
3628
|
return sessionService.getDistillerBudget();
|
|
3260
3629
|
},
|
|
3261
|
-
|
|
3262
|
-
// The native subagent deleteSession passes the bare key straight to
|
|
3263
|
-
// sessions.delete, which the 2026.6.x gateway indexes under the canonical
|
|
3264
|
-
// agent:<id>: form — the bare-key delete silently no-ops and the
|
|
3265
|
-
// excerpt-bearing transcript survives. deleteSessions() resolves the
|
|
3266
|
-
// canonical key via sessions.resolve first.
|
|
3630
|
+
|
|
3267
3631
|
deleteDistillerSession(sessionKey) {
|
|
3268
3632
|
return sessionService.deleteSessions("ocuclaw", [sessionKey]);
|
|
3269
3633
|
},
|
|
@@ -3284,11 +3648,6 @@ function createRelay(opts) {
|
|
|
3284
3648
|
return sessionService.peekSessionKey();
|
|
3285
3649
|
},
|
|
3286
3650
|
|
|
3287
|
-
/**
|
|
3288
|
-
* Test/shutdown hook: resolves once the async first-user-message cache
|
|
3289
|
-
* write has fully drained (no write in flight, no dirty mark pending) so
|
|
3290
|
-
* the on-disk file reflects the latest in-memory map.
|
|
3291
|
-
*/
|
|
3292
3651
|
flushFirstSentUserMessageCache() {
|
|
3293
3652
|
return sessionService.flushFirstSentUserMessageCache();
|
|
3294
3653
|
},
|
|
@@ -3305,20 +3664,10 @@ function createRelay(opts) {
|
|
|
3305
3664
|
return result;
|
|
3306
3665
|
},
|
|
3307
3666
|
|
|
3308
|
-
/**
|
|
3309
|
-
* Test-only: direct access to dispatchOcuClawUserSend so integration
|
|
3310
|
-
* tests can drive per-turn signal plumbing without a live downstream
|
|
3311
|
-
* WebSocket connection.
|
|
3312
|
-
*/
|
|
3313
3667
|
_dispatchOcuClawUserSend(params) {
|
|
3314
3668
|
return dispatchOcuClawUserSend(params || {});
|
|
3315
3669
|
},
|
|
3316
3670
|
|
|
3317
|
-
/**
|
|
3318
|
-
* Test-only: run the logical-reset state clear (the same call the /new,
|
|
3319
|
-
* /reset, and new-chat paths make) so integration tests can verify all
|
|
3320
|
-
* per-session state is dropped for a reused session key.
|
|
3321
|
-
*/
|
|
3322
3671
|
_clearLogicalSessionState(sessionKey) {
|
|
3323
3672
|
sessionService.clearLogicalSessionState(sessionKey);
|
|
3324
3673
|
},
|
|
@@ -3331,6 +3680,39 @@ function createRelay(opts) {
|
|
|
3331
3680
|
sendGlassesUiSurfaceUpdate(params);
|
|
3332
3681
|
},
|
|
3333
3682
|
|
|
3683
|
+
dispatchGlassesWake(params) {
|
|
3684
|
+
const sessionKey =
|
|
3685
|
+
params && typeof params.sessionKey === "string" && params.sessionKey
|
|
3686
|
+
? params.sessionKey
|
|
3687
|
+
: sessionService.ensureSessionKey();
|
|
3688
|
+
const message = params && typeof params.message === "string" ? params.message : "";
|
|
3689
|
+
if (!message) {
|
|
3690
|
+
return Promise.reject(new Error("dispatchGlassesWake requires a message"));
|
|
3691
|
+
}
|
|
3692
|
+
const idempotencyKey =
|
|
3693
|
+
params && typeof params.idempotencyKey === "string" && params.idempotencyKey
|
|
3694
|
+
? params.idempotencyKey
|
|
3695
|
+
: null;
|
|
3696
|
+
agentTurnTracker.markBusy(sessionKey);
|
|
3697
|
+
emitDebug(
|
|
3698
|
+
"relay.protocol",
|
|
3699
|
+
"glasses_wake_dispatch",
|
|
3700
|
+
"info",
|
|
3701
|
+
{ sessionKey },
|
|
3702
|
+
() => ({
|
|
3703
|
+
idempotencyKey,
|
|
3704
|
+
messageChars: message.length,
|
|
3705
|
+
}),
|
|
3706
|
+
);
|
|
3707
|
+
const requestParams = { message, sessionKey };
|
|
3708
|
+
if (idempotencyKey) requestParams.idempotencyKey = idempotencyKey;
|
|
3709
|
+
return gatewayBridge.request("agent", requestParams, { expectFinal: false });
|
|
3710
|
+
},
|
|
3711
|
+
|
|
3712
|
+
isAgentTurnBusy(sessionKey) {
|
|
3713
|
+
return agentTurnTracker.isBusy(sessionKey);
|
|
3714
|
+
},
|
|
3715
|
+
|
|
3334
3716
|
onGlassesUiResult(handler) {
|
|
3335
3717
|
return onGlassesUiResult(handler);
|
|
3336
3718
|
},
|
|
@@ -3351,6 +3733,10 @@ function createRelay(opts) {
|
|
|
3351
3733
|
return server ? server.getConnectedAppCount() > 0 : false;
|
|
3352
3734
|
},
|
|
3353
3735
|
|
|
3736
|
+
isGlassesSendBufferOverHighWater() {
|
|
3737
|
+
return glassesBackpressureLatch.isOverHighWater();
|
|
3738
|
+
},
|
|
3739
|
+
|
|
3354
3740
|
onAppClientDisconnect(handler) {
|
|
3355
3741
|
return onAppClientDisconnect(handler);
|
|
3356
3742
|
},
|