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.
Files changed (84) hide show
  1. package/README.md +29 -1
  2. package/dist/config/runtime-config-session-title-model.test.js +0 -3
  3. package/dist/config/runtime-config.js +22 -33
  4. package/dist/domain/activity-status-adapter.js +0 -7
  5. package/dist/domain/activity-status-arbiter.js +3 -27
  6. package/dist/domain/activity-status-labels.js +8 -38
  7. package/dist/domain/code-span-regions.js +4 -24
  8. package/dist/domain/constant-time-equal.js +9 -0
  9. package/dist/domain/constant-time-equal.test.js +28 -0
  10. package/dist/domain/conversation-state.js +27 -138
  11. package/dist/domain/debug-bundle-cache.js +52 -0
  12. package/dist/domain/debug-bundle-format.js +60 -0
  13. package/dist/domain/debug-bundle-preview.js +123 -0
  14. package/dist/domain/debug-bundle-redaction.js +182 -0
  15. package/dist/domain/debug-bundle-save.js +11 -0
  16. package/dist/domain/debug-bundle-zip.js +15 -0
  17. package/dist/domain/debug-bundle.js +97 -0
  18. package/dist/domain/debug-store.js +6 -17
  19. package/dist/domain/debug-upload-preset.js +27 -0
  20. package/dist/domain/glasses-display-system-prompt.js +0 -5
  21. package/dist/domain/glasses-display-system-prompt.test.js +1 -1
  22. package/dist/domain/glasses-ui-content-summary.js +0 -6
  23. package/dist/domain/glasses-ui-system-prompt.test.js +1 -2
  24. package/dist/domain/message-emoji-allowlist.js +0 -7
  25. package/dist/domain/message-emoji-filter.js +3 -9
  26. package/dist/domain/neural-emoji-reactor-tag-config.js +3 -3
  27. package/dist/domain/prompt-channel-fragments.js +1 -10
  28. package/dist/domain/tagged-span-parser.js +3 -26
  29. package/dist/domain/tagged-span-strip.js +0 -7
  30. package/dist/even-ai/even-ai-endpoint.js +77 -24
  31. package/dist/even-ai/even-ai-run-waiter.js +0 -1
  32. package/dist/even-ai/even-ai-settings-store.js +11 -0
  33. package/dist/gateway/gateway-bridge.js +8 -9
  34. package/dist/gateway/gateway-timing-ledger.js +8 -6
  35. package/dist/gateway/openclaw-client.js +97 -297
  36. package/dist/gateway/sanitize-connect-reason.js +10 -0
  37. package/dist/gateway/sanitize-connect-reason.test.js +34 -0
  38. package/dist/index.js +3 -3
  39. package/dist/runtime/channel-two-hook.js +1 -6
  40. package/dist/runtime/container-env.js +1 -5
  41. package/dist/runtime/debug-bundle-handler.js +159 -0
  42. package/dist/runtime/display-toggle-states.js +6 -17
  43. package/dist/runtime/downstream-handler.js +682 -508
  44. package/dist/runtime/glasses-backpressure-latch.js +93 -0
  45. package/dist/runtime/ocuclaw-settings-store.js +10 -1
  46. package/dist/runtime/openclaw-host-version.js +5 -0
  47. package/dist/runtime/plugin-version-service.js +13 -6
  48. package/dist/runtime/provider-usage-select.js +0 -6
  49. package/dist/runtime/register-session-title-distiller.js +14 -16
  50. package/dist/runtime/relay-core.js +657 -271
  51. package/dist/runtime/relay-service.js +40 -36
  52. package/dist/runtime/relay-worker-approval-replay-cache.js +1 -1
  53. package/dist/runtime/relay-worker-entry.js +1 -2
  54. package/dist/runtime/relay-worker-health.js +2 -10
  55. package/dist/runtime/relay-worker-protocol.js +6 -1
  56. package/dist/runtime/relay-worker-supervisor.js +109 -39
  57. package/dist/runtime/relay-worker-transport.js +157 -15
  58. package/dist/runtime/session-context-service.js +5 -45
  59. package/dist/runtime/session-service.js +157 -175
  60. package/dist/runtime/session-title-distiller-budget.js +1 -5
  61. package/dist/runtime/session-title-distiller-helpers.js +14 -24
  62. package/dist/runtime/session-title-distiller.js +109 -122
  63. package/dist/runtime/session-title-record.js +0 -6
  64. package/dist/runtime/stable-prompt-snapshot.js +3 -14
  65. package/dist/runtime/upstream-runtime.js +600 -103
  66. package/dist/tools/device-info-tool.js +4 -21
  67. package/dist/tools/glasses-ui-cron.js +58 -63
  68. package/dist/tools/glasses-ui-descriptors.js +4 -33
  69. package/dist/tools/glasses-ui-limits.js +0 -13
  70. package/dist/tools/glasses-ui-paint-floor.js +22 -34
  71. package/dist/tools/glasses-ui-recipes.js +92 -101
  72. package/dist/tools/glasses-ui-surfaces.js +295 -100
  73. package/dist/tools/glasses-ui-template.js +7 -22
  74. package/dist/tools/glasses-ui-tool-description.test.js +2 -2
  75. package/dist/tools/glasses-ui-tool.js +475 -331
  76. package/dist/tools/glasses-ui-voicemail.js +242 -0
  77. package/dist/tools/glasses-ui-wake.js +195 -0
  78. package/dist/tools/session-title-tool.js +2 -7
  79. package/dist/tools/session-title-tool.test.js +1 -1
  80. package/dist/version.js +3 -2
  81. package/openclaw.plugin.json +60 -13
  82. package/package.json +3 -2
  83. package/skills/glasses-ui/SKILL.md +19 -3
  84. package/dist/runtime/protocol-adapter.js +0 -387
@@ -1,16 +1,18 @@
1
- import { normalizeEvenAiRoutingMode } from "../even-ai/even-ai-settings-store.js";
1
+ import {
2
+ normalizeEvenAiRoutingMode,
3
+ normalizeEvenAiDefaultAgent,
4
+ } from "../even-ai/even-ai-settings-store.js";
2
5
  import {
3
6
  normalizeOcuClawDefaultModel,
4
7
  normalizeOcuClawDefaultThinking,
5
8
  normalizeOcuClawSystemPrompt,
9
+ normalizeOcuClawDefaultAgent,
6
10
  } from "./ocuclaw-settings-store.js";
7
11
  import {
8
12
  formatMainOperationReceived,
9
13
  formatSendAck,
10
14
  } from "./relay-worker-protocol.js";
11
15
 
12
- // --- Factory ---
13
-
14
16
  function normalizeLogger(logger) {
15
17
  if (!logger || typeof logger !== "object") {
16
18
  return console;
@@ -24,61 +26,12 @@ function normalizeLogger(logger) {
24
26
  };
25
27
  }
26
28
 
27
- /**
28
- * Create a downstream message handler.
29
- *
30
- * Transport-agnostic protocol logic for processing messages from
31
- * downstream clients (Even App, commander.html). Consumed by relay.js.
32
- *
33
- * @param {object} opts
34
- * @param {(id: string, text: string, sessionKey: string|null, attachment: object|null, clientDisplaySignals: object|null) => Promise} opts.onSend
35
- * Forward a user message to the upstream OpenClaw agent.
36
- * @param {(sender: string, text: string) => Array} opts.onSimulate
37
- * Inject a fake message into conversation state; returns pages array.
38
- * @param {(request: object) => Promise<object>|object} [opts.onSimulateStream]
39
- * Inject deterministic streaming payload (relay-local), then finalize pages.
40
- * @param {() => Promise<Array>} opts.onNewChat
41
- * Clear conversation and reset the OpenClaw session; returns empty pages array.
42
- * @param {() => Promise<{models: Array, fetchedAtMs: number, stale: boolean}>} [opts.onGetModelsCatalog]
43
- * Return cached model catalog snapshot.
44
- * @param {() => Promise<{skills: Array, fetchedAtMs: number, stale: boolean}>} [opts.onGetSkillsCatalog]
45
- * Return cached skills catalog snapshot.
46
- * @param {() => Promise<{models: Array, fetchedAtMs: number, stale: boolean}>} [opts.onGetSonioxModels]
47
- * Return cached Soniox model snapshot.
48
- * @param {() => Promise<object>} [opts.onGetProviderUsageSnapshot]
49
- * Return the current provider usage snapshot for the active provider.
50
- * @param {() => Promise<object>} [opts.onGetSessionModelConfig]
51
- * Return current session model controls.
52
- * @param {(patch: object) => Promise<{status: string, error?: string}>} [opts.onSetSessionModelConfig]
53
- * Patch current session model controls.
54
- * @param {({sessionKey: string}) => Promise<{status: string, error?: string}>} [opts.onCompactSession]
55
- * Trigger gateway-side compaction for a session key.
56
- * @param {() => Promise<object>} [opts.onGetEvenAiSettings]
57
- * Return current relay-owned Even AI settings.
58
- * @param {() => Promise<{sessions: Array, dedicatedKey: string}>} [opts.onGetEvenAiSessions]
59
- * Return the Even AI session-browser payload.
60
- * @param {(patch: object) => Promise<{status: string, error?: string, settings?: object}>} [opts.onSetEvenAiSettings]
61
- * Patch relay-owned Even AI settings.
62
- * @param {() => Promise<object>} [opts.onGetOcuClawSettings]
63
- * Return current relay-owned OcuClaw settings.
64
- * @param {(patch: object) => Promise<{status: string, error?: string, settings?: object}>} [opts.onSetOcuClawSettings]
65
- * Patch relay-owned OcuClaw settings.
66
- * @param {(clientId: string, payload: object) => Promise<object>|object} [opts.onRequestSonioxTemporaryKey]
67
- * Mint a short-lived Soniox temporary key for the provided voiceSessionId.
68
- * @param {() => object} [opts.onGetStatus]
69
- * Return the current relay status snapshot.
70
- * @param {() => boolean} opts.isUpstreamConnected
71
- * Returns true if the OpenClaw gateway connection is active.
72
- * @param {(clientId: string, payload: object) => void} [opts.onEventDebug]
73
- * Optional structured client debug-event callback.
74
- * @param {(clientId: string, payload: object) => Promise<object>|object} [opts.onReadinessProbe]
75
- * Optional dedicated readiness-probe dispatcher.
76
- * @returns {object} Handler instance
77
- */
78
29
  function createDownstreamHandler(opts) {
79
30
  const logger = normalizeLogger(opts.logger);
80
31
  const externalDebugToolsEnabled = opts.externalDebugToolsEnabled !== false;
81
32
  const onSend = opts.onSend;
33
+ const onAbortSession = opts.onAbortSession || null;
34
+ const onSteerSession = opts.onSteerSession || null;
82
35
  const onSimulate = opts.onSimulate;
83
36
  const onSimulateStream = opts.onSimulateStream || null;
84
37
  const onNewChat = opts.onNewChat;
@@ -88,10 +41,12 @@ function createDownstreamHandler(opts) {
88
41
  const onSlashCommand = opts.onSlashCommand;
89
42
  const onGetModelsCatalog = opts.onGetModelsCatalog;
90
43
  const onGetSkillsCatalog = opts.onGetSkillsCatalog;
44
+ const onGetAgentsCatalog = opts.onGetAgentsCatalog;
91
45
  const onGetSonioxModels = opts.onGetSonioxModels || null;
92
46
  const onGetProviderUsageSnapshot = opts.onGetProviderUsageSnapshot || null;
93
47
  const onGetSessionModelConfig = opts.onGetSessionModelConfig;
94
48
  const onSetSessionModelConfig = opts.onSetSessionModelConfig;
49
+ const onSetSessionAgent = opts.onSetSessionAgent;
95
50
  const onCompactSession = opts.onCompactSession || null;
96
51
  const onGetEvenAiSettings = opts.onGetEvenAiSettings;
97
52
  const onGetEvenAiSessions = opts.onGetEvenAiSessions;
@@ -99,6 +54,7 @@ function createDownstreamHandler(opts) {
99
54
  const onGetOcuClawSettings = opts.onGetOcuClawSettings;
100
55
  const onSetOcuClawSettings = opts.onSetOcuClawSettings;
101
56
  const onRequestSonioxTemporaryKey = opts.onRequestSonioxTemporaryKey || null;
57
+ const onRequestCartesiaAccessToken = opts.onRequestCartesiaAccessToken || null;
102
58
  const onGetStatus = opts.onGetStatus || null;
103
59
  const isUpstreamConnected = opts.isUpstreamConnected;
104
60
  const onConsoleLog = opts.onConsoleLog || null;
@@ -119,10 +75,12 @@ function createDownstreamHandler(opts) {
119
75
  const onSetSessionPinned = opts.onSetSessionPinned || null;
120
76
  const onDeleteSessions = opts.onDeleteSessions || null;
121
77
  const onSearchTranscripts = opts.onSearchTranscripts || null;
78
+ const onDebugBundleRequest = opts.onDebugBundleRequest || null;
79
+ const onDebugBundleSave = opts.onDebugBundleSave || null;
80
+ const onDebugBundleFetch = opts.onDebugBundleFetch || null;
122
81
  const getSnapshotRevision = opts.getSnapshotRevision || null;
123
82
  const operationRegistry = opts.operationRegistry || null;
124
83
 
125
- /** Client IDs subscribed to raw protocol frame forwarding. */
126
84
  const protocolSubscribers = new Set();
127
85
  const APPROVAL_DECISIONS = new Set(["allow-once", "allow-always", "deny"]);
128
86
  const approvalResolveCacheTtlMs = Number.isFinite(opts.approvalResolveCacheTtlMs)
@@ -133,7 +91,7 @@ function createDownstreamHandler(opts) {
133
91
  : 500;
134
92
  const EXTERNAL_DEBUG_TOOLS_DISABLED_ERROR =
135
93
  "external debug tools are disabled by plugin config";
136
- /** @type {Map<string, {expiresAtMs: number, promise: Promise<object>}>} */
94
+
137
95
  const approvalResolveCache = new Map();
138
96
  const APP_PROTOCOL = {
139
97
  activity: "ocuclaw.activity.update",
@@ -165,6 +123,8 @@ function createDownstreamHandler(opts) {
165
123
  providerUsageSnapshot: "ocuclaw.provider.usage.snapshot",
166
124
  skillsCatalogGet: "ocuclaw.skills.catalog.get",
167
125
  skillsCatalogSnapshot: "ocuclaw.skills.catalog.snapshot",
126
+ agentsCatalogGet: "ocuclaw.agent.catalog.get",
127
+ agentsCatalogSnapshot: "ocuclaw.agent.catalog.snapshot",
168
128
  pages: "ocuclaw.view.pages.snapshot",
169
129
  protocolSubscribe: "ocuclaw.protocol.tap.subscribe",
170
130
  protocolFrame: "ocuclaw.protocol.tap.frame",
@@ -172,37 +132,38 @@ function createDownstreamHandler(opts) {
172
132
  readinessProbeRequest: "ocuclaw.readiness.probe.request",
173
133
  remoteControl: "ocuclaw.remote.control",
174
134
  requestSonioxTemporaryKey: "requestSonioxTemporaryKey",
135
+ requestCartesiaAccessToken: "requestCartesiaAccessToken",
175
136
  sonioxModelsGet: "ocuclaw.voice.soniox.models.get",
176
137
  sonioxModelsSnapshot: "ocuclaw.voice.soniox.models.snapshot",
177
138
  sessionConfigGet: "ocuclaw.session.config.get",
178
139
  sessionConfigSet: "ocuclaw.session.config.set",
179
140
  sessionConfigSetAck: "ocuclaw.session.config.set.ack",
180
141
  sessionConfigSnapshot: "ocuclaw.session.config.snapshot",
142
+ sessionAgentSet: "ocuclaw.session.agent.set",
143
+ sessionAgentSetAck: "ocuclaw.session.agent.set.ack",
144
+ sessionAbort: "ocuclaw.session.abort",
145
+ sessionAbortAck: "ocuclaw.session.abort.ack",
181
146
  sessionCompact: "ocuclaw.session.compact",
182
147
  sessionCompactAck: "ocuclaw.session.compact.ack",
183
148
  sessionCreate: "ocuclaw.session.create",
184
149
  sessionList: "ocuclaw.session.list",
150
+ sessionListDiff: "ocuclaw.session.list.diff",
151
+ sessionListDiffResult: "ocuclaw.session.list.diff.result",
185
152
  sessionListResult: "ocuclaw.session.list.result",
186
153
  sessionReset: "ocuclaw.session.reset",
154
+ sessionSteer: "ocuclaw.session.steer",
187
155
  sessionSwitch: "ocuclaw.session.switch",
188
156
  sessionSwitchApplied: "ocuclaw.session.switch.applied",
189
157
  sessionTitleSet: "ocuclaw.session.title.set",
190
158
  sonioxTemporaryKey: "sonioxTemporaryKey",
191
159
  sonioxTemporaryKeyError: "sonioxTemporaryKeyError",
160
+ cartesiaAccessToken: "cartesiaAccessToken",
161
+ cartesiaAccessTokenError: "cartesiaAccessTokenError",
192
162
  status: "ocuclaw.runtime.status",
193
163
  statusGet: "ocuclaw.runtime.status.get",
194
164
  typingUpdate: "ocuclaw.typing.update",
195
165
  };
196
166
 
197
- // --- Format helpers ---
198
-
199
- /**
200
- * Format a pages message for broadcast.
201
- *
202
- * @param {Array<{content: string, subPage: [number,number]|null}>} pages
203
- * @param {{revision?: number}} [meta]
204
- * @returns {string} JSON string
205
- */
206
167
  function formatPages(pages, meta) {
207
168
  const msg = { type: APP_PROTOCOL.pages, pages };
208
169
  const fallbackRevision = getSnapshotRevision
@@ -219,13 +180,6 @@ function createDownstreamHandler(opts) {
219
180
  return JSON.stringify(msg);
220
181
  }
221
182
 
222
- /**
223
- * Format a status message for broadcast.
224
- *
225
- * @param {object} status - Status fields (openclaw, agent, session, etc.)
226
- * @param {{revision?: number}} [meta]
227
- * @returns {string} JSON string
228
- */
229
183
  function formatStatus(status, meta) {
230
184
  const msg = { ...status, type: APP_PROTOCOL.status };
231
185
  const fallbackRevision = getSnapshotRevision
@@ -242,33 +196,14 @@ function createDownstreamHandler(opts) {
242
196
  return JSON.stringify(msg);
243
197
  }
244
198
 
245
- /**
246
- * Format an activity message for broadcast.
247
- *
248
- * @param {object} activity - Activity fields (state, tool, etc.)
249
- * @returns {string} JSON string
250
- */
251
199
  function formatActivity(activity) {
252
200
  return JSON.stringify({ ...activity, type: APP_PROTOCOL.activity });
253
201
  }
254
202
 
255
- /**
256
- * Format a typing update message for broadcast.
257
- *
258
- * @param {object} update - Typing fields (state, runId, sessionKey, etc.)
259
- * @returns {string} JSON string
260
- */
261
203
  function formatTyping(update) {
262
204
  return JSON.stringify({ ...update, type: APP_PROTOCOL.typingUpdate });
263
205
  }
264
206
 
265
- /**
266
- * Format an error message for unicast.
267
- *
268
- * @param {string} error
269
- * @param {{code?: string, requestId?: string, op?: string}} [meta]
270
- * @returns {string} JSON string
271
- */
272
207
  function formatError(error, meta) {
273
208
  const msg = {
274
209
  type: "error",
@@ -301,30 +236,25 @@ function createDownstreamHandler(opts) {
301
236
  );
302
237
  }
303
238
 
304
- /**
305
- * Format a send acknowledgement for unicast.
306
- *
307
- * @param {string} id - Message ID being acknowledged
308
- * @param {string} status - "accepted" or "rejected"
309
- * @param {string} [error] - Error message (for rejected)
310
- * @param {string} [errorCode] - Structured error code (for deterministic client handling)
311
- * @returns {string} JSON string
312
- */
313
- function formatSendAckCompat(id, status, error, errorCode) {
314
- return formatSendAck(id, status, error, errorCode);
239
+ function formatSendAckCompat(id, status, error, errorCode, data) {
240
+ return formatSendAck(id, status, error, errorCode, data);
241
+ }
242
+
243
+ function formatSessionAbortAck(data = {}) {
244
+ const msg = {
245
+ type: APP_PROTOCOL.sessionAbortAck,
246
+ requestId: parseOptionalTrimmedString(data.requestId),
247
+ status: data.status || "accepted",
248
+ };
249
+ if (data.error !== undefined) msg.error = data.error;
250
+ if (data.errorCode !== undefined) msg.errorCode = data.errorCode;
251
+ return JSON.stringify(msg);
315
252
  }
316
253
 
317
254
  function formatOperationReceived(data) {
318
255
  return formatMainOperationReceived(data);
319
256
  }
320
257
 
321
- /**
322
- * Format a protocol frame for forwarding to subscribers.
323
- *
324
- * @param {string} direction - "in" or "out"
325
- * @param {object} frame - Raw protocol frame
326
- * @returns {string} JSON string
327
- */
328
258
  function formatProtocol(direction, frame) {
329
259
  return JSON.stringify({
330
260
  type: APP_PROTOCOL.protocolFrame,
@@ -333,13 +263,6 @@ function createDownstreamHandler(opts) {
333
263
  });
334
264
  }
335
265
 
336
- /**
337
- * Format a streaming text message for broadcast during agent runs.
338
- *
339
- * @param {string} text - Streaming assistant text
340
- * @param {Array<{start: number, end: number, emoji: string}>} [spans] - Optional emoji spans
341
- * @returns {string} JSON string
342
- */
343
266
  function formatStreaming(text, emojiSpans, paceSpans) {
344
267
  const payload = { type: APP_PROTOCOL.messageStreamDelta, text };
345
268
  if (Array.isArray(emojiSpans) && emojiSpans.length > 0) {
@@ -351,22 +274,135 @@ function createDownstreamHandler(opts) {
351
274
  return JSON.stringify(payload);
352
275
  }
353
276
 
354
- /**
355
- * Format a sessions list message for unicast response.
356
- *
357
- * @param {Array<{key: string, updatedAt: number, preview: string, firstUserMessage?: string}>} sessions
358
- * @returns {string} JSON string
359
- */
360
277
  function formatSessions(sessions) {
361
278
  return JSON.stringify({ type: APP_PROTOCOL.sessionListResult, sessions });
362
279
  }
363
280
 
364
- /**
365
- * Format a session-switched confirmation for broadcast.
366
- *
367
- * @param {string} sessionKey
368
- * @returns {string} JSON string
369
- */
281
+ function sessionInfoFingerprint(session) {
282
+ const row = session && typeof session === "object" ? session : {};
283
+ const raw = [
284
+ row.key || "",
285
+ Number.isFinite(Number(row.updatedAt)) ? String(Math.floor(Number(row.updatedAt))) : "0",
286
+ row.preview || "",
287
+ row.firstUserMessage || "",
288
+ row.title || "",
289
+ row.pinned === true ? "true" : "false",
290
+ Number.isFinite(Number(row.pinnedAtMs)) ? String(Math.floor(Number(row.pinnedAtMs))) : "",
291
+ row.agentId || "",
292
+ row.agentName || "",
293
+ ].join("\u001f");
294
+ return fnv1a32Hex(raw);
295
+ }
296
+
297
+ function fnv1a32Hex(text) {
298
+ let hash = 0x811c9dc5;
299
+ for (let i = 0; i < text.length; i += 1) {
300
+ hash ^= text.charCodeAt(i);
301
+ hash = Math.imul(hash, 0x01000193);
302
+ }
303
+ return (hash >>> 0).toString(16).padStart(8, "0");
304
+ }
305
+
306
+ function normalizeSessionDiffLimit(limit) {
307
+ if (!Number.isFinite(Number(limit)) || Number(limit) <= 0) return 80;
308
+ return Math.min(200, Math.max(1, Math.floor(Number(limit))));
309
+ }
310
+
311
+ function normalizeSessionDiffKind(kind) {
312
+ return String(kind || "").trim().toLowerCase() === "evenai"
313
+ ? "evenai"
314
+ : "ocuclaw";
315
+ }
316
+
317
+ function parseKnownSessionRows(msg) {
318
+ if (!msg || !Array.isArray(msg.known)) return [];
319
+ const out = [];
320
+ for (const item of msg.known) {
321
+ if (!item || typeof item !== "object") continue;
322
+ const key = typeof item.key === "string" ? item.key.trim() : "";
323
+ if (!key) continue;
324
+ out.push({
325
+ key,
326
+ updatedAt: Number.isFinite(Number(item.updatedAt))
327
+ ? Math.floor(Number(item.updatedAt))
328
+ : 0,
329
+ fingerprint:
330
+ typeof item.fingerprint === "string" ? item.fingerprint.trim() : "",
331
+ });
332
+ }
333
+ return out;
334
+ }
335
+
336
+ function buildSessionDiff({ kind, sessions, known, limit, dedicatedKey }) {
337
+ const normalizedKind = normalizeSessionDiffKind(kind);
338
+ const normalizedLimit = normalizeSessionDiffLimit(limit);
339
+ const rows = Array.isArray(sessions) ? sessions : [];
340
+ const limitedRows = rows
341
+ .slice()
342
+ .sort((left, right) => (Number(right && right.updatedAt) || 0) - (Number(left && left.updatedAt) || 0))
343
+ .slice(0, normalizedLimit);
344
+ const knownByKey = new Map();
345
+ for (const row of Array.isArray(known) ? known : []) {
346
+ const key = typeof row.key === "string" ? row.key.trim().toLowerCase() : "";
347
+ if (!key) continue;
348
+ knownByKey.set(key, row);
349
+ }
350
+ const liveKeys = new Set();
351
+ const changed = [];
352
+ for (const row of limitedRows) {
353
+ const key = typeof row.key === "string" ? row.key.trim().toLowerCase() : "";
354
+ if (!key) continue;
355
+ liveKeys.add(key);
356
+ const knownRow = knownByKey.get(key);
357
+ const updatedAt = Number.isFinite(Number(row.updatedAt))
358
+ ? Math.floor(Number(row.updatedAt))
359
+ : 0;
360
+ const fingerprint = sessionInfoFingerprint(row);
361
+ if (
362
+ !knownRow ||
363
+ knownRow.updatedAt !== updatedAt ||
364
+ knownRow.fingerprint !== fingerprint
365
+ ) {
366
+ changed.push(row);
367
+ }
368
+ }
369
+ const deletedKeys = [];
370
+ for (const row of Array.isArray(known) ? known : []) {
371
+ const rawKey = typeof row.key === "string" ? row.key.trim() : "";
372
+ const key = rawKey.toLowerCase();
373
+ if (key && !liveKeys.has(key)) deletedKeys.push(rawKey);
374
+ }
375
+ const out = {
376
+ type: APP_PROTOCOL.sessionListDiffResult,
377
+ kind: normalizedKind,
378
+ sessions: changed,
379
+ deletedKeys,
380
+ limit: normalizedLimit,
381
+ };
382
+ if (typeof dedicatedKey === "string" && dedicatedKey) {
383
+ out.dedicatedKey = dedicatedKey;
384
+ }
385
+ return out;
386
+ }
387
+
388
+ function formatSessionDiff(payload) {
389
+ return JSON.stringify(buildSessionDiff(payload || {}));
390
+ }
391
+
392
+ function formatEmptySessionDiff(kind, limit, dedicatedKey) {
393
+ const out = {
394
+ type: APP_PROTOCOL.sessionListDiffResult,
395
+ kind: normalizeSessionDiffKind(kind),
396
+ sessions: [],
397
+ deletedKeys: [],
398
+ limit: normalizeSessionDiffLimit(limit),
399
+ };
400
+ if (typeof dedicatedKey === "string" && dedicatedKey) {
401
+ out.dedicatedKey = dedicatedKey;
402
+ }
403
+ return JSON.stringify(out);
404
+ }
405
+
370
406
  function formatSessionSwitched(sessionKey) {
371
407
  return JSON.stringify({
372
408
  type: APP_PROTOCOL.sessionSwitchApplied,
@@ -374,12 +410,6 @@ function createDownstreamHandler(opts) {
374
410
  });
375
411
  }
376
412
 
377
- /**
378
- * Format a model catalog snapshot for unicast.
379
- *
380
- * @param {{models: Array, fetchedAtMs: number, stale: boolean}} payload
381
- * @returns {string}
382
- */
383
413
  function formatModelsCatalog(payload) {
384
414
  return JSON.stringify({
385
415
  type: APP_PROTOCOL.modelCatalogSnapshot,
@@ -392,12 +422,6 @@ function createDownstreamHandler(opts) {
392
422
  });
393
423
  }
394
424
 
395
- /**
396
- * Format a cached skills catalog snapshot for unicast.
397
- *
398
- * @param {{skills?: Array, fetchedAtMs?: number, stale?: boolean}} payload
399
- * @returns {string}
400
- */
401
425
  function formatSkillsCatalog(payload) {
402
426
  return JSON.stringify({
403
427
  type: APP_PROTOCOL.skillsCatalogSnapshot,
@@ -410,12 +434,27 @@ function createDownstreamHandler(opts) {
410
434
  });
411
435
  }
412
436
 
413
- /**
414
- * Format a Soniox model snapshot for unicast.
415
- *
416
- * @param {{models: Array, fetchedAtMs: number, stale: boolean}} payload
417
- * @returns {string}
418
- */
437
+ function formatAgentsCatalog(payload) {
438
+ return JSON.stringify({
439
+ type: APP_PROTOCOL.agentsCatalogSnapshot,
440
+ agents: Array.isArray(payload && payload.agents) ? payload.agents : [],
441
+ defaultId:
442
+ payload && typeof payload.defaultId === "string"
443
+ ? payload.defaultId
444
+ : null,
445
+ mainKey:
446
+ payload && typeof payload.mainKey === "string" ? payload.mainKey : null,
447
+ scope:
448
+ payload && typeof payload.scope === "string" ? payload.scope : null,
449
+ fetchedAtMs:
450
+ Number.isFinite(payload && payload.fetchedAtMs)
451
+ ? Math.floor(payload.fetchedAtMs)
452
+ : Date.now(),
453
+ stale: !!(payload && payload.stale),
454
+ unsupported: !!(payload && payload.unsupported),
455
+ });
456
+ }
457
+
419
458
  function formatSonioxModels(payload) {
420
459
  return JSON.stringify({
421
460
  type: APP_PROTOCOL.sonioxModelsSnapshot,
@@ -428,12 +467,6 @@ function createDownstreamHandler(opts) {
428
467
  });
429
468
  }
430
469
 
431
- /**
432
- * Format a provider usage snapshot for unicast.
433
- *
434
- * @param {object} payload
435
- * @returns {string}
436
- */
437
470
  function formatProviderUsageSnapshot(payload) {
438
471
  const provider =
439
472
  payload && typeof payload.provider === "string" && payload.provider.trim()
@@ -497,12 +530,6 @@ function createDownstreamHandler(opts) {
497
530
  });
498
531
  }
499
532
 
500
- /**
501
- * Format current session model controls.
502
- *
503
- * @param {object} payload
504
- * @returns {string}
505
- */
506
533
  function formatSessionModelConfig(payload) {
507
534
  return JSON.stringify({
508
535
  type: APP_PROTOCOL.sessionConfigSnapshot,
@@ -530,15 +557,11 @@ function createDownstreamHandler(opts) {
530
557
  payload && typeof payload.elevatedLevel === "string"
531
558
  ? payload.elevatedLevel
532
559
  : "off",
560
+ agentId:
561
+ payload && typeof payload.agentId === "string" ? payload.agentId : "",
533
562
  });
534
563
  }
535
564
 
536
- /**
537
- * Format a session model config write acknowledgement.
538
- *
539
- * @param {{status: string, error?: string}} payload
540
- * @returns {string}
541
- */
542
565
  function formatSessionModelConfigAck(payload) {
543
566
  const out = {
544
567
  type: APP_PROTOCOL.sessionConfigSetAck,
@@ -553,12 +576,6 @@ function createDownstreamHandler(opts) {
553
576
  return JSON.stringify(out);
554
577
  }
555
578
 
556
- /**
557
- * Format a compactSession ack message for unicast.
558
- *
559
- * @param {{status: string, error?: string, requestId?: string}} payload
560
- * @returns {string} JSON string
561
- */
562
579
  function formatCompactSessionAck(payload) {
563
580
  const msg = {
564
581
  type: APP_PROTOCOL.sessionCompactAck,
@@ -577,12 +594,6 @@ function createDownstreamHandler(opts) {
577
594
  return JSON.stringify(msg);
578
595
  }
579
596
 
580
- /**
581
- * Format the current relay-owned Even AI settings snapshot.
582
- *
583
- * @param {object} payload
584
- * @returns {string}
585
- */
586
597
  function formatEvenAiSettings(payload) {
587
598
  return JSON.stringify({
588
599
  type: APP_PROTOCOL.evenAiSettingsSnapshot,
@@ -605,15 +616,14 @@ function createDownstreamHandler(opts) {
605
616
  : "",
606
617
  listenEnabled: payload && payload.listenEnabled === true,
607
618
  defaultFastMode: !!(payload && payload.defaultFastMode === true),
619
+ defaultAgent: normalizeEvenAiDefaultAgent(
620
+ payload && typeof payload.defaultAgent === "string"
621
+ ? payload.defaultAgent
622
+ : undefined,
623
+ ),
608
624
  });
609
625
  }
610
626
 
611
- /**
612
- * Format an Even AI settings write acknowledgement.
613
- *
614
- * @param {{status: string, error?: string}} payload
615
- * @returns {string}
616
- */
617
627
  function formatEvenAiSettingsAck(payload) {
618
628
  const out = {
619
629
  type: APP_PROTOCOL.evenAiSettingsSetAck,
@@ -628,12 +638,6 @@ function createDownstreamHandler(opts) {
628
638
  return JSON.stringify(out);
629
639
  }
630
640
 
631
- /**
632
- * Format the current relay-owned OcuClaw settings snapshot.
633
- *
634
- * @param {object} payload
635
- * @returns {string}
636
- */
637
641
  function formatOcuClawSettings(payload) {
638
642
  return JSON.stringify({
639
643
  type: APP_PROTOCOL.ocuClawSettingsSnapshot,
@@ -653,15 +657,14 @@ function createDownstreamHandler(opts) {
653
657
  : undefined,
654
658
  ),
655
659
  defaultFastMode: !!(payload && payload.defaultFastMode === true),
660
+ defaultAgent: normalizeOcuClawDefaultAgent(
661
+ payload && typeof payload.defaultAgent === "string"
662
+ ? payload.defaultAgent
663
+ : undefined,
664
+ ),
656
665
  });
657
666
  }
658
667
 
659
- /**
660
- * Format an OcuClaw settings write acknowledgement.
661
- *
662
- * @param {{status: string, error?: string}} payload
663
- * @returns {string}
664
- */
665
668
  function formatOcuClawSettingsAck(payload) {
666
669
  const out = {
667
670
  type: APP_PROTOCOL.ocuClawSettingsSetAck,
@@ -676,12 +679,6 @@ function createDownstreamHandler(opts) {
676
679
  return JSON.stringify(out);
677
680
  }
678
681
 
679
- /**
680
- * Format the Even AI session-browser result payload.
681
- *
682
- * @param {{sessions?: Array, dedicatedKey?: string}} payload
683
- * @returns {string}
684
- */
685
682
  function formatEvenAiSessions(payload) {
686
683
  return JSON.stringify({
687
684
  type: APP_PROTOCOL.evenAiSessionListResult,
@@ -693,12 +690,6 @@ function createDownstreamHandler(opts) {
693
690
  });
694
691
  }
695
692
 
696
- /**
697
- * Format an exec approval request for broadcast.
698
- *
699
- * @param {object} data - Approval payload from gateway
700
- * @returns {string} JSON string
701
- */
702
693
  function formatApproval(data) {
703
694
  const request = data && data.request ? data.request : {};
704
695
  const approvalKind =
@@ -750,12 +741,6 @@ function createDownstreamHandler(opts) {
750
741
  });
751
742
  }
752
743
 
753
- /**
754
- * Format an exec approval resolution for broadcast.
755
- *
756
- * @param {object} data - Resolution payload from gateway
757
- * @returns {string} JSON string
758
- */
759
744
  function formatApprovalResolved(data) {
760
745
  return JSON.stringify({
761
746
  type: APP_PROTOCOL.approvalResolved,
@@ -768,12 +753,6 @@ function createDownstreamHandler(opts) {
768
753
  });
769
754
  }
770
755
 
771
- /**
772
- * Format an approval-response acknowledgement for unicast.
773
- *
774
- * @param {object} data
775
- * @returns {string}
776
- */
777
756
  function formatApprovalResponseAck(data) {
778
757
  return JSON.stringify({
779
758
  type: APP_PROTOCOL.approvalResolveAck,
@@ -802,14 +781,6 @@ function createDownstreamHandler(opts) {
802
781
  });
803
782
  }
804
783
 
805
- /**
806
- * Format a committed listen handoff update for broadcast during voice mode.
807
- *
808
- * @param {string} text - Final committed text sent upstream
809
- * @param {"manual"|"endpoint"} source - Commit source path
810
- * @param {string|null} sessionKey - Active relay session key for diagnostics
811
- * @returns {string} JSON string
812
- */
813
784
  function formatListenCommitted(text, source, sessionKey) {
814
785
  return JSON.stringify({
815
786
  type: "listen-committed",
@@ -826,20 +797,10 @@ function createDownstreamHandler(opts) {
826
797
  });
827
798
  }
828
799
 
829
- /**
830
- * Format a listen-ended message for broadcast.
831
- * @returns {string} JSON string
832
- */
833
800
  function formatListenEnded() {
834
801
  return JSON.stringify({ type: "listen-ended" });
835
802
  }
836
803
 
837
- /**
838
- * Format a listen-error message for broadcast.
839
- * @param {string} error - Error description
840
- * @param {string|null} [code] - Structured error code
841
- * @returns {string} JSON string
842
- */
843
804
  function formatListenError(error, code = null) {
844
805
  const msg = { type: "listen-error", error };
845
806
  if (typeof code === "string" && code.trim()) {
@@ -848,20 +809,10 @@ function createDownstreamHandler(opts) {
848
809
  return JSON.stringify(msg);
849
810
  }
850
811
 
851
- /**
852
- * Format a listen-ready message for broadcast (agent done, client can auto-restart).
853
- * @returns {string} JSON string
854
- */
855
812
  function formatListenReady() {
856
813
  return JSON.stringify({ type: "listen-ready" });
857
814
  }
858
815
 
859
- /**
860
- * Format a temporary Soniox key lease for unicast back to the requesting client.
861
- *
862
- * @param {{voiceSessionId: string, temporaryKey: string, expiresAtMs: number}} payload
863
- * @returns {string}
864
- */
865
816
  function formatSonioxTemporaryKey(payload) {
866
817
  return JSON.stringify({
867
818
  type: APP_PROTOCOL.sonioxTemporaryKey,
@@ -880,12 +831,6 @@ function createDownstreamHandler(opts) {
880
831
  });
881
832
  }
882
833
 
883
- /**
884
- * Format a Soniox temporary-key failure for unicast back to the requesting client.
885
- *
886
- * @param {{voiceSessionId: string, error: string, code?: string|null}} payload
887
- * @returns {string}
888
- */
889
834
  function formatSonioxTemporaryKeyError(payload) {
890
835
  const msg = {
891
836
  type: APP_PROTOCOL.sonioxTemporaryKeyError,
@@ -908,6 +853,88 @@ function createDownstreamHandler(opts) {
908
853
  return JSON.stringify(msg);
909
854
  }
910
855
 
856
+ function parseRequestCartesiaAccessToken(msg) {
857
+ if (!msg || typeof msg !== "object") {
858
+ throw new Error("requestCartesiaAccessToken payload must be an object");
859
+ }
860
+
861
+ const voiceSessionId = parseOptionalTrimmedString(msg.voiceSessionId);
862
+ if (!voiceSessionId) {
863
+ throw new Error("voiceSessionId is required");
864
+ }
865
+
866
+ return {
867
+ voiceSessionId,
868
+ sessionKey: parseOptionalTrimmedString(msg.sessionKey),
869
+ };
870
+ }
871
+
872
+ function normalizeCartesiaAccessTokenErrorCode(err) {
873
+
874
+ const explicit = err && typeof err.code === "string" ? err.code.trim() : "";
875
+ if (explicit) return explicit;
876
+
877
+ const message =
878
+ err && typeof err.message === "string" && err.message.trim()
879
+ ? err.message.trim()
880
+ : "";
881
+ const lowered = message.toLowerCase();
882
+ if (!message) return "cartesia_access_token_failed";
883
+
884
+ if (err && err.name === "AbortError") {
885
+ return "cartesia_access_token_mint_timeout";
886
+ }
887
+ if (lowered.includes("api key is not configured")) {
888
+ return "cartesia_access_token_not_configured";
889
+ }
890
+ if (lowered.includes("fetch is not available")) {
891
+ return "cartesia_access_token_fetch_unavailable";
892
+ }
893
+ if (lowered.includes("voicesessionid is required")) {
894
+ return "cartesia_access_token_invalid_request";
895
+ }
896
+ if (lowered.includes("missing token")) {
897
+ return "cartesia_access_token_invalid_response";
898
+ }
899
+ const statusMatch = lowered.match(/\((\d{3})\)/);
900
+ if (statusMatch) {
901
+ return `cartesia_access_token_http_${statusMatch[1]}`;
902
+ }
903
+ return "cartesia_access_token_failed";
904
+ }
905
+
906
+ function formatCartesiaAccessToken(payload) {
907
+ return JSON.stringify({
908
+ type: APP_PROTOCOL.cartesiaAccessToken,
909
+ voiceSessionId:
910
+ payload && typeof payload.voiceSessionId === "string" ? payload.voiceSessionId : "",
911
+ accessToken:
912
+ payload && typeof payload.accessToken === "string" ? payload.accessToken : "",
913
+ expiresAtMs:
914
+ payload && Number.isFinite(payload.expiresAtMs) ? Math.floor(payload.expiresAtMs) : 0,
915
+ });
916
+ }
917
+
918
+ function formatCartesiaAccessTokenError(payload) {
919
+ const msg = {
920
+ type: APP_PROTOCOL.cartesiaAccessTokenError,
921
+ voiceSessionId:
922
+ payload && typeof payload.voiceSessionId === "string" ? payload.voiceSessionId : "",
923
+ error:
924
+ payload && typeof payload.error === "string" && payload.error.trim()
925
+ ? payload.error.trim()
926
+ : "Cartesia access-token request failed",
927
+ };
928
+ const code =
929
+ payload && typeof payload.code === "string" && payload.code.trim()
930
+ ? payload.code.trim()
931
+ : "";
932
+ if (code) {
933
+ msg.code = code;
934
+ }
935
+ return JSON.stringify(msg);
936
+ }
937
+
911
938
  function normalizeSonioxTemporaryKeyErrorCode(err) {
912
939
  const message =
913
940
  err && typeof err.message === "string" && err.message.trim()
@@ -915,7 +942,7 @@ function createDownstreamHandler(opts) {
915
942
  : "";
916
943
  const lowered = message.toLowerCase();
917
944
  if (!message) return "soniox_temp_key_request_failed";
918
- // AbortError from the per-fetch timeout AbortController in mintSonioxTemporaryKey.
945
+
919
946
  if (err && err.name === "AbortError") {
920
947
  return "soniox_temp_key_mint_timeout";
921
948
  }
@@ -941,12 +968,6 @@ function createDownstreamHandler(opts) {
941
968
  return "soniox_temp_key_request_failed";
942
969
  }
943
970
 
944
- /**
945
- * Format a debug-set response for unicast.
946
- *
947
- * @param {object} data
948
- * @returns {string}
949
- */
950
971
  function formatDebugSet(data) {
951
972
  return JSON.stringify({
952
973
  type: "debug-set",
@@ -954,12 +975,6 @@ function createDownstreamHandler(opts) {
954
975
  });
955
976
  }
956
977
 
957
- /**
958
- * Format a debug-dump response for unicast.
959
- *
960
- * @param {object} data
961
- * @returns {string}
962
- */
963
978
  function formatDebugDump(data) {
964
979
  return JSON.stringify({
965
980
  type: "debug-dump",
@@ -967,10 +982,6 @@ function createDownstreamHandler(opts) {
967
982
  });
968
983
  }
969
984
 
970
- /**
971
- * Parse a "trace-log-set" message.
972
- * @returns {{enabled: boolean}}
973
- */
974
985
  function parseTraceLogSet(msg) {
975
986
  if (!msg || typeof msg !== "object") {
976
987
  throw new Error("trace-log-set requires an object");
@@ -981,19 +992,10 @@ function createDownstreamHandler(opts) {
981
992
  return { enabled: msg.enabled };
982
993
  }
983
994
 
984
- /**
985
- * Format a trace-log response for unicast (set + get share one type).
986
- */
987
995
  function formatTraceLog(data) {
988
996
  return JSON.stringify({ type: "trace-log", ...data });
989
997
  }
990
998
 
991
- /**
992
- * Format the current relay debug-config snapshot for app/WebUI clients.
993
- *
994
- * @param {{serverNowMs: number, enabled: Array<{cat: string, expiresAtMs: number}>}} data
995
- * @returns {string}
996
- */
997
999
  function formatDebugConfigSnapshot(data) {
998
1000
  const enabled = Array.isArray(data && data.enabled)
999
1001
  ? data.enabled
@@ -1019,12 +1021,6 @@ function createDownstreamHandler(opts) {
1019
1021
  });
1020
1022
  }
1021
1023
 
1022
- /**
1023
- * Format a remote-control command for broadcast to app clients.
1024
- *
1025
- * @param {object} data
1026
- * @returns {string}
1027
- */
1028
1024
  function formatRemoteControl(data) {
1029
1025
  return JSON.stringify({
1030
1026
  type: APP_PROTOCOL.remoteControl,
@@ -1032,12 +1028,6 @@ function createDownstreamHandler(opts) {
1032
1028
  });
1033
1029
  }
1034
1030
 
1035
- /**
1036
- * Format a remote-control acknowledgement for unicast.
1037
- *
1038
- * @param {object} data
1039
- * @returns {string}
1040
- */
1041
1031
  function formatRemoteControlAck(data) {
1042
1032
  return JSON.stringify({
1043
1033
  type: "remote-control-ack",
@@ -1626,6 +1616,13 @@ function createDownstreamHandler(opts) {
1626
1616
  payload.defaultFastMode = msg.defaultFastMode;
1627
1617
  }
1628
1618
 
1619
+ if (Object.prototype.hasOwnProperty.call(msg, "defaultAgent")) {
1620
+ if (typeof msg.defaultAgent !== "string") {
1621
+ throw new Error("defaultAgent must be a string");
1622
+ }
1623
+ payload.defaultAgent = normalizeEvenAiDefaultAgent(msg.defaultAgent);
1624
+ }
1625
+
1629
1626
  if (Object.keys(payload).length === 0) {
1630
1627
  throw new Error("setEvenAiSettings requires at least one field");
1631
1628
  }
@@ -1668,6 +1665,13 @@ function createDownstreamHandler(opts) {
1668
1665
  payload.defaultFastMode = msg.defaultFastMode;
1669
1666
  }
1670
1667
 
1668
+ if (Object.prototype.hasOwnProperty.call(msg, "defaultAgent")) {
1669
+ if (typeof msg.defaultAgent !== "string") {
1670
+ throw new Error("defaultAgent must be a string");
1671
+ }
1672
+ payload.defaultAgent = normalizeOcuClawDefaultAgent(msg.defaultAgent);
1673
+ }
1674
+
1671
1675
  if (Object.keys(payload).length === 0) {
1672
1676
  throw new Error("setOcuClawSettings requires at least one field");
1673
1677
  }
@@ -2006,11 +2010,6 @@ function createDownstreamHandler(opts) {
2006
2010
  };
2007
2011
  }
2008
2012
 
2009
- /**
2010
- * Validate and normalize an inbound clientDisplaySignals envelope.
2011
- * Returns null if absent or unparseable. Falls back to safe defaults
2012
- * for individual fields rather than rejecting the whole envelope.
2013
- */
2014
2013
  function parseClientDisplaySignals(raw) {
2015
2014
  if (raw == null || typeof raw !== "object") return null;
2016
2015
  const coerceState = (val) => {
@@ -2024,7 +2023,7 @@ function createDownstreamHandler(opts) {
2024
2023
  const enabledRaw = raw.neuralSessionNamesEnabled;
2025
2024
  const neuralSessionNamesEnabled =
2026
2025
  typeof enabledRaw === "boolean" ? enabledRaw : true;
2027
- // readSpeedWpm tolerated but ignored on inbound for old clients.
2026
+
2028
2027
  return {
2029
2028
  neuralEmojiReactorState: state,
2030
2029
  neuralPaceModulatorState: paceState,
@@ -2032,15 +2031,6 @@ function createDownstreamHandler(opts) {
2032
2031
  };
2033
2032
  }
2034
2033
 
2035
- // --- Message handlers ---
2036
-
2037
- /**
2038
- * Handle a "send" message: forward user text to upstream agent.
2039
- *
2040
- * @param {string} clientId
2041
- * @param {object} msg - Parsed message with id, text, sessionKey, and optional attachment
2042
- * @returns {{ unicast: string }|Promise<{ unicast: string }>}
2043
- */
2044
2034
  function handleSend(clientId, msg) {
2045
2035
  const requestId = parseOptionalTrimmedString(msg.requestId);
2046
2036
  if (!requestId) {
@@ -2076,7 +2066,6 @@ function createDownstreamHandler(opts) {
2076
2066
  };
2077
2067
  }
2078
2068
 
2079
- // Check upstream connectivity
2080
2069
  if (!isUpstreamConnected()) {
2081
2070
  return {
2082
2071
  unicast: formatSendAckCompat(
@@ -2105,7 +2094,6 @@ function createDownstreamHandler(opts) {
2105
2094
 
2106
2095
  const clientDisplaySignals = parseClientDisplaySignals(msg.clientDisplaySignals);
2107
2096
 
2108
- // Forward to upstream — resolves on initial ack (accepted/queued)
2109
2097
  const followup = onSend(
2110
2098
  requestId,
2111
2099
  text,
@@ -2115,9 +2103,18 @@ function createDownstreamHandler(opts) {
2115
2103
  ).then(
2116
2104
  (result) => {
2117
2105
  const status = (result && result.status) || "accepted";
2118
- const frame = formatSendAckCompat(requestId, status);
2106
+ const frame = formatSendAckCompat(
2107
+ requestId,
2108
+ status,
2109
+ undefined,
2110
+ undefined,
2111
+ { runId: result && result.runId },
2112
+ );
2119
2113
  if (operation && typeof operation.complete === "function") {
2120
- operation.complete(frame, { status });
2114
+ operation.complete(frame, {
2115
+ status,
2116
+ runId: result && result.runId ? result.runId : null,
2117
+ });
2121
2118
  }
2122
2119
  return { unicast: frame };
2123
2120
  },
@@ -2149,13 +2146,149 @@ function createDownstreamHandler(opts) {
2149
2146
  : followup;
2150
2147
  }
2151
2148
 
2152
- /**
2153
- * Handle a "simulate" message: inject fake message into conversation state.
2154
- *
2155
- * @param {string} clientId
2156
- * @param {object} msg - Parsed message with sender, text
2157
- * @returns {{ broadcast: string }}
2158
- */
2149
+ function handleAbortSession(clientId, msg) {
2150
+ const requestId = parseOptionalTrimmedString(msg.requestId);
2151
+ if (!requestId) {
2152
+ return {
2153
+ unicast: formatSessionAbortAck({
2154
+ requestId,
2155
+ status: "rejected",
2156
+ error: "Missing required field: requestId",
2157
+ }),
2158
+ };
2159
+ }
2160
+ const sessionKey = parseOptionalTrimmedString(msg.sessionKey);
2161
+ if (!sessionKey) {
2162
+ return {
2163
+ unicast: formatSessionAbortAck({
2164
+ requestId,
2165
+ status: "rejected",
2166
+ error: "Missing required field: sessionKey",
2167
+ }),
2168
+ };
2169
+ }
2170
+ if (!onAbortSession) {
2171
+ return {
2172
+ unicast: formatSessionAbortAck({
2173
+ requestId,
2174
+ status: "rejected",
2175
+ error: "session abort is not available",
2176
+ }),
2177
+ };
2178
+ }
2179
+ if (!isUpstreamConnected()) {
2180
+ return {
2181
+ unicast: formatSessionAbortAck({
2182
+ requestId,
2183
+ status: "rejected",
2184
+ error: "OpenClaw disconnected",
2185
+ }),
2186
+ };
2187
+ }
2188
+ return Promise.resolve(onAbortSession({ requestId, sessionKey })).then(
2189
+ (result) => ({
2190
+ unicast: formatSessionAbortAck({
2191
+ requestId,
2192
+ ...(result || { status: "accepted" }),
2193
+ }),
2194
+ }),
2195
+ (err) => ({
2196
+ unicast: formatSessionAbortAck({
2197
+ requestId,
2198
+ status: "rejected",
2199
+ error: err && err.message ? err.message : "session abort failed",
2200
+ errorCode: err && (err.errorCode || err.code) ? (err.errorCode || err.code) : undefined,
2201
+ }),
2202
+ }),
2203
+ );
2204
+ }
2205
+
2206
+ function handleSteerSession(clientId, msg) {
2207
+ const requestId = parseOptionalTrimmedString(msg.requestId);
2208
+ if (!requestId) {
2209
+ return {
2210
+ unicast: formatSendAckCompat(
2211
+ requestId,
2212
+ "rejected",
2213
+ "Missing required field: requestId",
2214
+ ),
2215
+ };
2216
+ }
2217
+ const sessionKey = parseOptionalTrimmedString(msg.sessionKey);
2218
+ if (!sessionKey) {
2219
+ return {
2220
+ unicast: formatSendAckCompat(
2221
+ requestId,
2222
+ "rejected",
2223
+ "Missing required field: sessionKey",
2224
+ ),
2225
+ };
2226
+ }
2227
+ const parsedAttachment = parseAttachment(msg.attachment);
2228
+ if (!parsedAttachment.ok) {
2229
+ return {
2230
+ unicast: formatSendAckCompat(
2231
+ requestId,
2232
+ "rejected",
2233
+ parsedAttachment.error,
2234
+ parsedAttachment.errorCode,
2235
+ ),
2236
+ };
2237
+ }
2238
+ const message = typeof msg.message === "string" ? msg.message : "";
2239
+ if (!message.trim() && !parsedAttachment.attachment) {
2240
+ return {
2241
+ unicast: formatSendAckCompat(
2242
+ requestId,
2243
+ "rejected",
2244
+ "Missing required field: message",
2245
+ ),
2246
+ };
2247
+ }
2248
+ if (!onSteerSession) {
2249
+ return {
2250
+ unicast: formatSendAckCompat(
2251
+ requestId,
2252
+ "rejected",
2253
+ "session steer is not available",
2254
+ ),
2255
+ };
2256
+ }
2257
+ if (!isUpstreamConnected()) {
2258
+ return {
2259
+ unicast: formatSendAckCompat(
2260
+ requestId,
2261
+ "rejected",
2262
+ "OpenClaw disconnected",
2263
+ ),
2264
+ };
2265
+ }
2266
+ return Promise.resolve(onSteerSession({
2267
+ requestId,
2268
+ sessionKey,
2269
+ message,
2270
+ attachment: parsedAttachment.attachment,
2271
+ })).then(
2272
+ (result) => ({
2273
+ unicast: formatSendAckCompat(
2274
+ requestId,
2275
+ (result && result.status) || "accepted",
2276
+ undefined,
2277
+ undefined,
2278
+ { runId: result && result.runId },
2279
+ ),
2280
+ }),
2281
+ (err) => ({
2282
+ unicast: formatSendAckCompat(
2283
+ requestId,
2284
+ "rejected",
2285
+ err && err.message ? err.message : "session steer failed",
2286
+ err && (err.errorCode || err.code) ? (err.errorCode || err.code) : undefined,
2287
+ ),
2288
+ }),
2289
+ );
2290
+ }
2291
+
2159
2292
  function handleSimulate(clientId, msg) {
2160
2293
  const pages = onSimulate(
2161
2294
  msg.sender || "Simulator",
@@ -2164,13 +2297,6 @@ function createDownstreamHandler(opts) {
2164
2297
  return { broadcast: formatPages(pages) };
2165
2298
  }
2166
2299
 
2167
- /**
2168
- * Handle a "simulateStream" message: relay-local deterministic stream replay.
2169
- *
2170
- * @param {string} clientId
2171
- * @param {object} msg - Parsed message with id, text, sender, and timing controls.
2172
- * @returns {{ unicast: string }|Promise<{ unicast: string }>}
2173
- */
2174
2300
  function handleSimulateStream(clientId, msg) {
2175
2301
  const id = parseOptionalTrimmedString(msg.id);
2176
2302
  if (!id) {
@@ -2250,24 +2376,11 @@ function createDownstreamHandler(opts) {
2250
2376
  );
2251
2377
  }
2252
2378
 
2253
- /**
2254
- * Handle a "subscribeProtocol" message: mark client for protocol forwarding.
2255
- *
2256
- * @param {string} clientId
2257
- * @returns {null}
2258
- */
2259
2379
  function handleSubscribeProtocol(clientId) {
2260
2380
  protocolSubscribers.add(clientId);
2261
2381
  return null;
2262
2382
  }
2263
2383
 
2264
- /**
2265
- * Handle an "approvalResponse" message: forward decision to upstream gateway.
2266
- *
2267
- * @param {string} clientId
2268
- * @param {object} msg - Parsed message with id and decision
2269
- * @returns {{ unicast: string }|Promise<{ unicast: string }>}
2270
- */
2271
2384
  function handleApprovalResponse(clientId, msg) {
2272
2385
  let payload;
2273
2386
  try {
@@ -2373,12 +2486,6 @@ function createDownstreamHandler(opts) {
2373
2486
  });
2374
2487
  }
2375
2488
 
2376
- /**
2377
- * Handle a "newChat" message: clear conversation and reset the session.
2378
- *
2379
- * @param {string} clientId
2380
- * @returns {Promise<{ broadcast: string }>}
2381
- */
2382
2489
  function handleNewChat(clientId) {
2383
2490
  return onNewChat().then(
2384
2491
  (pages) => ({ broadcast: formatPages(pages) }),
@@ -2389,12 +2496,6 @@ function createDownstreamHandler(opts) {
2389
2496
  );
2390
2497
  }
2391
2498
 
2392
- /**
2393
- * Handle a "getSessions" message: fetch session list from upstream.
2394
- *
2395
- * @param {string} clientId
2396
- * @returns {Promise<{ unicast: string }>}
2397
- */
2398
2499
  function handleGetSessions(clientId) {
2399
2500
  return onGetSessions().then(
2400
2501
  (sessions) => ({ unicast: formatSessions(sessions) }),
@@ -2405,12 +2506,46 @@ function createDownstreamHandler(opts) {
2405
2506
  );
2406
2507
  }
2407
2508
 
2408
- /**
2409
- * Handle "getModelsCatalog": return cached model catalog snapshot.
2410
- *
2411
- * @param {string} clientId
2412
- * @returns {Promise<{ unicast: string }>|{ unicast: string }}
2413
- */
2509
+ function handleGetSessionDiff(clientId, msg) {
2510
+ const kind = normalizeSessionDiffKind(msg && msg.kind);
2511
+ const known = parseKnownSessionRows(msg);
2512
+ const limit = normalizeSessionDiffLimit(msg && msg.limit);
2513
+ if (kind === "evenai") {
2514
+ if (!onGetEvenAiSessions) {
2515
+ return Promise.resolve({
2516
+ unicast: formatEmptySessionDiff(kind, limit, "ocuclaw:even-ai"),
2517
+ });
2518
+ }
2519
+ return Promise.resolve(onGetEvenAiSessions()).then(
2520
+ (payload) => ({
2521
+ unicast: formatSessionDiff({
2522
+ kind,
2523
+ sessions: payload && payload.sessions,
2524
+ known,
2525
+ limit,
2526
+ dedicatedKey:
2527
+ payload && typeof payload.dedicatedKey === "string"
2528
+ ? payload.dedicatedKey
2529
+ : "ocuclaw:even-ai",
2530
+ }),
2531
+ }),
2532
+ (err) => {
2533
+ logger.error(`[downstream] getEvenAiSessionDiff failed: ${err.message}`);
2534
+ return { unicast: formatEmptySessionDiff(kind, limit, "ocuclaw:even-ai") };
2535
+ },
2536
+ );
2537
+ }
2538
+ return onGetSessions().then(
2539
+ (sessions) => ({
2540
+ unicast: formatSessionDiff({ kind, sessions, known, limit }),
2541
+ }),
2542
+ (err) => {
2543
+ logger.error(`[downstream] getSessionDiff failed: ${err.message}`);
2544
+ return { unicast: formatEmptySessionDiff(kind, limit) };
2545
+ },
2546
+ );
2547
+ }
2548
+
2414
2549
  function handleGetModelsCatalog(clientId) {
2415
2550
  if (!onGetModelsCatalog) {
2416
2551
  return {
@@ -2438,12 +2573,6 @@ function createDownstreamHandler(opts) {
2438
2573
  );
2439
2574
  }
2440
2575
 
2441
- /**
2442
- * Handle "getSkills": return cached skills catalog snapshot.
2443
- *
2444
- * @param {string} clientId
2445
- * @returns {Promise<{ unicast: string }>|{ unicast: string }}
2446
- */
2447
2576
  function handleGetSkillsCatalog(clientId) {
2448
2577
  if (!onGetSkillsCatalog) {
2449
2578
  return {
@@ -2471,12 +2600,34 @@ function createDownstreamHandler(opts) {
2471
2600
  );
2472
2601
  }
2473
2602
 
2474
- /**
2475
- * Handle "getSonioxModels": return cached Soniox model snapshot.
2476
- *
2477
- * @param {string} clientId
2478
- * @returns {Promise<{ unicast: string }>|{ unicast: string }}
2479
- */
2603
+ function handleGetAgentsCatalog(clientId) {
2604
+ if (!onGetAgentsCatalog) {
2605
+ return {
2606
+ unicast: formatAgentsCatalog({
2607
+ agents: [],
2608
+ fetchedAtMs: Date.now(),
2609
+ stale: true,
2610
+ unsupported: true,
2611
+ }),
2612
+ };
2613
+ }
2614
+ return Promise.resolve(onGetAgentsCatalog()).then(
2615
+ (payload) => ({
2616
+ unicast: formatAgentsCatalog(payload || {}),
2617
+ }),
2618
+ (err) => {
2619
+ logger.error(`[downstream] getAgentsCatalog failed: ${err.message}`);
2620
+ return {
2621
+ unicast: formatAgentsCatalog({
2622
+ agents: [],
2623
+ fetchedAtMs: Date.now(),
2624
+ stale: true,
2625
+ }),
2626
+ };
2627
+ },
2628
+ );
2629
+ }
2630
+
2480
2631
  function handleGetSonioxModels(clientId) {
2481
2632
  if (!onGetSonioxModels) {
2482
2633
  return {
@@ -2504,12 +2655,6 @@ function createDownstreamHandler(opts) {
2504
2655
  );
2505
2656
  }
2506
2657
 
2507
- /**
2508
- * Handle "getProviderUsageSnapshot": return the current provider usage snapshot.
2509
- *
2510
- * @param {string} clientId
2511
- * @returns {Promise<{ unicast: string }>|{ unicast: string }}
2512
- */
2513
2658
  function handleGetProviderUsageSnapshot(clientId) {
2514
2659
  const emptySnapshot = () => ({
2515
2660
  sessionKey: null,
@@ -2539,12 +2684,6 @@ function createDownstreamHandler(opts) {
2539
2684
  );
2540
2685
  }
2541
2686
 
2542
- /**
2543
- * Handle "getStatus": return the current status snapshot.
2544
- *
2545
- * @param {string} clientId
2546
- * @returns {{ unicast: string }}
2547
- */
2548
2687
  function handleGetStatus(clientId) {
2549
2688
  if (!onGetStatus) {
2550
2689
  return { unicast: formatError("getStatus is not available") };
@@ -2558,12 +2697,6 @@ function createDownstreamHandler(opts) {
2558
2697
  }
2559
2698
  }
2560
2699
 
2561
- /**
2562
- * Handle "getSessionModelConfig": read controls for current session key.
2563
- *
2564
- * @param {string} clientId
2565
- * @returns {Promise<{ unicast: string }>|{ unicast: string }}
2566
- */
2567
2700
  function handleGetSessionModelConfig(clientId) {
2568
2701
  if (!onGetSessionModelConfig) {
2569
2702
  return { unicast: formatError("getSessionModelConfig is not available") };
@@ -2579,13 +2712,66 @@ function createDownstreamHandler(opts) {
2579
2712
  );
2580
2713
  }
2581
2714
 
2582
- /**
2583
- * Handle "setSessionModelConfig": patch controls for current session key.
2584
- *
2585
- * @param {string} clientId
2586
- * @param {object} msg
2587
- * @returns {Promise<{ unicast: string }>|{ unicast: string }}
2588
- */
2715
+ function formatSessionAgentAck(payload) {
2716
+ const out = {
2717
+ type: APP_PROTOCOL.sessionAgentSetAck,
2718
+ status:
2719
+ payload && typeof payload.status === "string"
2720
+ ? payload.status
2721
+ : "rejected",
2722
+ };
2723
+ if (payload && payload.error !== undefined) {
2724
+ out.error = payload.error;
2725
+ }
2726
+ return JSON.stringify(out);
2727
+ }
2728
+
2729
+ function parseSetSessionAgent(msg) {
2730
+ if (!msg || typeof msg !== "object") {
2731
+ throw new Error("setSessionAgent payload must be an object");
2732
+ }
2733
+ if (!Object.prototype.hasOwnProperty.call(msg, "agentId")) {
2734
+ throw new Error("setSessionAgent requires an agentId field");
2735
+ }
2736
+ if (msg.agentId !== null && typeof msg.agentId !== "string") {
2737
+ throw new Error("agentId must be a string or null");
2738
+ }
2739
+ return { agentId: typeof msg.agentId === "string" ? msg.agentId.trim() : "" };
2740
+ }
2741
+
2742
+ function handleSetSessionAgent(clientId, msg) {
2743
+ if (!onSetSessionAgent) {
2744
+ return {
2745
+ unicast: formatSessionAgentAck({
2746
+ status: "rejected",
2747
+ error: "setSessionAgent is not available",
2748
+ }),
2749
+ };
2750
+ }
2751
+ let payload;
2752
+ try {
2753
+ payload = parseSetSessionAgent(msg);
2754
+ } catch (err) {
2755
+ return {
2756
+ unicast: formatSessionAgentAck({
2757
+ status: "rejected",
2758
+ error: err && err.message ? err.message : "invalid setSessionAgent payload",
2759
+ }),
2760
+ };
2761
+ }
2762
+ return Promise.resolve(onSetSessionAgent(payload)).then(
2763
+ (result) => ({
2764
+ unicast: formatSessionAgentAck(result || { status: "accepted" }),
2765
+ }),
2766
+ (err) => ({
2767
+ unicast: formatSessionAgentAck({
2768
+ status: "rejected",
2769
+ error: err && err.message ? err.message : "setSessionAgent failed",
2770
+ }),
2771
+ }),
2772
+ );
2773
+ }
2774
+
2589
2775
  function handleSetSessionModelConfig(clientId, msg) {
2590
2776
  if (!onSetSessionModelConfig) {
2591
2777
  return {
@@ -2623,13 +2809,6 @@ function createDownstreamHandler(opts) {
2623
2809
  );
2624
2810
  }
2625
2811
 
2626
- /**
2627
- * Handle "compactSession": trigger gateway-side compaction for a session key.
2628
- *
2629
- * @param {string} clientId
2630
- * @param {object} msg - { sessionKey, requestId }
2631
- * @returns {Promise<{ unicast: string }>}
2632
- */
2633
2812
  function handleCompactSession(clientId, msg) {
2634
2813
  if (!onCompactSession) {
2635
2814
  return Promise.resolve({
@@ -2670,12 +2849,6 @@ function createDownstreamHandler(opts) {
2670
2849
  );
2671
2850
  }
2672
2851
 
2673
- /**
2674
- * Handle "getEvenAiSettings": read relay-owned Even AI settings.
2675
- *
2676
- * @param {string} clientId
2677
- * @returns {Promise<{ unicast: string }>|{ unicast: string }}
2678
- */
2679
2852
  function handleGetEvenAiSettings(clientId) {
2680
2853
  if (!onGetEvenAiSettings) {
2681
2854
  return { unicast: formatError("getEvenAiSettings is not available") };
@@ -2691,12 +2864,6 @@ function createDownstreamHandler(opts) {
2691
2864
  );
2692
2865
  }
2693
2866
 
2694
- /**
2695
- * Handle "getEvenAiSessions": return tracked Even AI sessions.
2696
- *
2697
- * @param {string} clientId
2698
- * @returns {Promise<{ unicast: string }>|{ unicast: string }}
2699
- */
2700
2867
  function handleGetEvenAiSessions(clientId) {
2701
2868
  if (!onGetEvenAiSessions) {
2702
2869
  return { unicast: formatError("getEvenAiSessions is not available") };
@@ -2712,13 +2879,6 @@ function createDownstreamHandler(opts) {
2712
2879
  );
2713
2880
  }
2714
2881
 
2715
- /**
2716
- * Handle "setEvenAiSettings": patch relay-owned Even AI settings.
2717
- *
2718
- * @param {string} clientId
2719
- * @param {object} msg
2720
- * @returns {Promise<{ unicast: string }>|{ unicast: string }}
2721
- */
2722
2882
  function handleSetEvenAiSettings(clientId, msg) {
2723
2883
  if (!onSetEvenAiSettings) {
2724
2884
  return {
@@ -2754,12 +2914,6 @@ function createDownstreamHandler(opts) {
2754
2914
  );
2755
2915
  }
2756
2916
 
2757
- /**
2758
- * Handle "getOcuClawSettings": read relay-owned OcuClaw settings.
2759
- *
2760
- * @param {string} clientId
2761
- * @returns {Promise<{ unicast: string }>|{ unicast: string }}
2762
- */
2763
2917
  function handleGetOcuClawSettings(clientId) {
2764
2918
  if (!onGetOcuClawSettings) {
2765
2919
  return { unicast: formatError("getOcuClawSettings is not available") };
@@ -2775,13 +2929,6 @@ function createDownstreamHandler(opts) {
2775
2929
  );
2776
2930
  }
2777
2931
 
2778
- /**
2779
- * Handle "setOcuClawSettings": patch relay-owned OcuClaw settings.
2780
- *
2781
- * @param {string} clientId
2782
- * @param {object} msg
2783
- * @returns {Promise<{ unicast: string }>|{ unicast: string }}
2784
- */
2785
2932
  function handleSetOcuClawSettings(clientId, msg) {
2786
2933
  if (!onSetOcuClawSettings) {
2787
2934
  return {
@@ -2817,13 +2964,6 @@ function createDownstreamHandler(opts) {
2817
2964
  );
2818
2965
  }
2819
2966
 
2820
- /**
2821
- * Handle a "switchSession" message: switch to a different session.
2822
- *
2823
- * @param {string} clientId
2824
- * @param {object} msg - Parsed message with sessionKey
2825
- * @returns {Promise<{ broadcast: string[] }|null>}
2826
- */
2827
2967
  function handleSwitchSession(clientId, msg) {
2828
2968
  if (!msg.sessionKey) return null;
2829
2969
  return onSwitchSession(msg.sessionKey).then(
@@ -2840,12 +2980,6 @@ function createDownstreamHandler(opts) {
2840
2980
  );
2841
2981
  }
2842
2982
 
2843
- /**
2844
- * Handle a "newSession" message: create a new session.
2845
- *
2846
- * @param {string} clientId
2847
- * @returns {Promise<{ broadcast: string[] }|null>}
2848
- */
2849
2983
  function handleNewSession(clientId) {
2850
2984
  return onNewSession().then(
2851
2985
  (result) => {
@@ -2865,14 +2999,6 @@ function createDownstreamHandler(opts) {
2865
2999
  );
2866
3000
  }
2867
3001
 
2868
- /**
2869
- * Handle a "session.title.set" message: user-edited session title from
2870
- * phone UI. Sticky-locks the agent against retitling.
2871
- *
2872
- * @param {string} clientId
2873
- * @param {object} msg - Parsed message with sessionKey and title
2874
- * @returns {null}
2875
- */
2876
3002
  function handleSetUserSessionTitle(clientId, msg) {
2877
3003
  if (typeof onSetUserSessionTitle !== "function") return null;
2878
3004
  const sessionKey =
@@ -2924,13 +3050,6 @@ function createDownstreamHandler(opts) {
2924
3050
  return null;
2925
3051
  }
2926
3052
 
2927
- /**
2928
- * Handle a "slashCommand" message: forward a slash command to OpenClaw.
2929
- *
2930
- * @param {string} clientId
2931
- * @param {object} msg - Parsed message with command
2932
- * @returns {Promise<null>}
2933
- */
2934
3053
  function handleSlashCommand(clientId, msg) {
2935
3054
  if (!msg.command) return null;
2936
3055
  return onSlashCommand(msg.command).then(
@@ -2942,13 +3061,6 @@ function createDownstreamHandler(opts) {
2942
3061
  );
2943
3062
  }
2944
3063
 
2945
- /**
2946
- * Handle a "console" message: forward browser console output to log.
2947
- *
2948
- * @param {string} clientId
2949
- * @param {object} msg - Parsed message with level, message
2950
- * @returns {null}
2951
- */
2952
3064
  function handleConsole(clientId, msg) {
2953
3065
  if (onConsoleLog) {
2954
3066
  onConsoleLog(msg.level || "log", msg.message || "");
@@ -2956,11 +3068,6 @@ function createDownstreamHandler(opts) {
2956
3068
  return null;
2957
3069
  }
2958
3070
 
2959
- /**
2960
- * Handle a structured "eventDebug" message.
2961
- *
2962
- * Legacy payloads (without cat/event) fall back to onConsoleLog.
2963
- */
2964
3071
  function handleEventDebug(clientId, msg) {
2965
3072
  const parsed = parseEventDebug(msg);
2966
3073
  if (parsed && onEventDebug) {
@@ -2983,9 +3090,6 @@ function createDownstreamHandler(opts) {
2983
3090
  };
2984
3091
  }
2985
3092
 
2986
- /**
2987
- * Handle a "requestSonioxTemporaryKey" message.
2988
- */
2989
3093
  function handleRequestSonioxTemporaryKey(clientId, msg) {
2990
3094
  let payload;
2991
3095
  try {
@@ -3043,9 +3147,57 @@ function createDownstreamHandler(opts) {
3043
3147
  }
3044
3148
  }
3045
3149
 
3046
- /**
3047
- * Handle a "debug-set" message: enable/disable debug categories with TTL.
3048
- */
3150
+ function handleRequestCartesiaAccessToken(clientId, msg) {
3151
+ let payload;
3152
+ try {
3153
+ payload = parseRequestCartesiaAccessToken(msg);
3154
+ } catch (err) {
3155
+ return {
3156
+ unicast: formatCartesiaAccessTokenError({
3157
+ voiceSessionId:
3158
+ msg && typeof msg.voiceSessionId === "string" ? msg.voiceSessionId : "",
3159
+ error: err && err.message ? err.message : "requestCartesiaAccessToken failed",
3160
+ code: normalizeCartesiaAccessTokenErrorCode(err),
3161
+ }),
3162
+ };
3163
+ }
3164
+
3165
+ if (!onRequestCartesiaAccessToken) {
3166
+ return {
3167
+ unicast: formatCartesiaAccessTokenError({
3168
+ voiceSessionId: payload.voiceSessionId,
3169
+ error: "requestCartesiaAccessToken is not available",
3170
+ code: "cartesia_access_token_unavailable",
3171
+ }),
3172
+ };
3173
+ }
3174
+
3175
+ try {
3176
+ const result = onRequestCartesiaAccessToken(clientId, payload);
3177
+ if (result && typeof result.then === "function") {
3178
+ return result.then(
3179
+ (resolved) => ({ unicast: formatCartesiaAccessToken(resolved || payload) }),
3180
+ (err) => ({
3181
+ unicast: formatCartesiaAccessTokenError({
3182
+ voiceSessionId: payload.voiceSessionId,
3183
+ error: err && err.message ? err.message : "requestCartesiaAccessToken failed",
3184
+ code: normalizeCartesiaAccessTokenErrorCode(err),
3185
+ }),
3186
+ }),
3187
+ );
3188
+ }
3189
+ return { unicast: formatCartesiaAccessToken(result || payload) };
3190
+ } catch (err) {
3191
+ return {
3192
+ unicast: formatCartesiaAccessTokenError({
3193
+ voiceSessionId: payload.voiceSessionId,
3194
+ error: err && err.message ? err.message : "requestCartesiaAccessToken failed",
3195
+ code: normalizeCartesiaAccessTokenErrorCode(err),
3196
+ }),
3197
+ };
3198
+ }
3199
+ }
3200
+
3049
3201
  function handleDebugSet(clientId, msg) {
3050
3202
  if (!onDebugSet) {
3051
3203
  return { unicast: formatError("debug-set is not available") };
@@ -3098,9 +3250,6 @@ function createDownstreamHandler(opts) {
3098
3250
  }
3099
3251
  }
3100
3252
 
3101
- /**
3102
- * Handle a "debug-dump" message: fetch structured debug events.
3103
- */
3104
3253
  function handleDebugDump(clientId, msg) {
3105
3254
  if (!onDebugDump) {
3106
3255
  return { unicast: formatError("debug-dump is not available") };
@@ -3157,9 +3306,6 @@ function createDownstreamHandler(opts) {
3157
3306
  }
3158
3307
  }
3159
3308
 
3160
- /**
3161
- * Handle a "remote-control" message: dispatch remote actions to app clients.
3162
- */
3163
3309
  function handleRemoteControl(clientId, msg) {
3164
3310
  if (!onRemoteControl) {
3165
3311
  return { unicast: formatError("remote-control is not available") };
@@ -3340,16 +3486,8 @@ function createDownstreamHandler(opts) {
3340
3486
  }
3341
3487
  }
3342
3488
 
3343
- // --- Public API ---
3344
-
3345
3489
  return {
3346
- /**
3347
- * Process an incoming downstream message.
3348
- *
3349
- * @param {string} clientId - Identifier of the sending client
3350
- * @param {string} raw - Raw JSON string
3351
- * @returns {{ unicast: string }|{ broadcast: string }|null|Promise}
3352
- */
3490
+
3353
3491
  handleMessage(clientId, raw) {
3354
3492
  let msg;
3355
3493
  try {
@@ -3372,6 +3510,10 @@ function createDownstreamHandler(opts) {
3372
3510
  switch (msg.type) {
3373
3511
  case APP_PROTOCOL.messageSend:
3374
3512
  return handleSend(clientId, msg);
3513
+ case APP_PROTOCOL.sessionAbort:
3514
+ return handleAbortSession(clientId, msg);
3515
+ case APP_PROTOCOL.sessionSteer:
3516
+ return handleSteerSession(clientId, msg);
3375
3517
  case "simulate":
3376
3518
  return handleSimulate(clientId, msg);
3377
3519
  case "simulateStream":
@@ -3384,6 +3526,8 @@ function createDownstreamHandler(opts) {
3384
3526
  return handleNewChat(clientId);
3385
3527
  case APP_PROTOCOL.sessionList:
3386
3528
  return handleGetSessions(clientId);
3529
+ case APP_PROTOCOL.sessionListDiff:
3530
+ return handleGetSessionDiff(clientId, msg);
3387
3531
  case APP_PROTOCOL.sessionSwitch:
3388
3532
  return handleSwitchSession(clientId, msg);
3389
3533
  case APP_PROTOCOL.sessionCreate:
@@ -3401,6 +3545,9 @@ function createDownstreamHandler(opts) {
3401
3545
  case APP_PROTOCOL.skillsCatalogGet:
3402
3546
  case "getSkills":
3403
3547
  return handleGetSkillsCatalog(clientId);
3548
+ case APP_PROTOCOL.agentsCatalogGet:
3549
+ case "getAgentsCatalog":
3550
+ return handleGetAgentsCatalog(clientId);
3404
3551
  case APP_PROTOCOL.sonioxModelsGet:
3405
3552
  case "getSonioxModels":
3406
3553
  return handleGetSonioxModels(clientId);
@@ -3414,6 +3561,9 @@ function createDownstreamHandler(opts) {
3414
3561
  return handleGetSessionModelConfig(clientId);
3415
3562
  case APP_PROTOCOL.sessionConfigSet:
3416
3563
  return handleSetSessionModelConfig(clientId, msg);
3564
+ case APP_PROTOCOL.sessionAgentSet:
3565
+ case "setSessionAgent":
3566
+ return handleSetSessionAgent(clientId, msg);
3417
3567
  case APP_PROTOCOL.sessionCompact:
3418
3568
  return handleCompactSession(clientId, msg);
3419
3569
  case APP_PROTOCOL.evenAiSettingsGet:
@@ -3437,10 +3587,46 @@ function createDownstreamHandler(opts) {
3437
3587
  return handleRemovedListenAction(msg.type);
3438
3588
  case APP_PROTOCOL.requestSonioxTemporaryKey:
3439
3589
  return handleRequestSonioxTemporaryKey(clientId, msg);
3590
+ case APP_PROTOCOL.requestCartesiaAccessToken:
3591
+ return handleRequestCartesiaAccessToken(clientId, msg);
3440
3592
  case "debug-set":
3441
3593
  return handleDebugSet(clientId, msg);
3442
3594
  case "debug-dump":
3443
3595
  return handleDebugDump(clientId, msg);
3596
+ case "debug-bundle-request":
3597
+
3598
+ if (typeof onDebugBundleRequest === "function") {
3599
+ try {
3600
+ onDebugBundleRequest(clientId, msg);
3601
+ } catch (err) {
3602
+ logger.warn(
3603
+ `[downstream] debug-bundle-request handler threw: ${err && err.message ? err.message : err}`,
3604
+ );
3605
+ }
3606
+ }
3607
+ return null;
3608
+ case "debug-bundle-save":
3609
+ if (typeof onDebugBundleSave === "function") {
3610
+ try {
3611
+ onDebugBundleSave(clientId, msg);
3612
+ } catch (err) {
3613
+ logger.warn(
3614
+ `[downstream] debug-bundle-save handler threw: ${err && err.message ? err.message : err}`,
3615
+ );
3616
+ }
3617
+ }
3618
+ return null;
3619
+ case "debug-bundle-fetch":
3620
+ if (typeof onDebugBundleFetch === "function") {
3621
+ try {
3622
+ onDebugBundleFetch(clientId, msg);
3623
+ } catch (err) {
3624
+ logger.warn(
3625
+ `[downstream] debug-bundle-fetch handler threw: ${err && err.message ? err.message : err}`,
3626
+ );
3627
+ }
3628
+ }
3629
+ return null;
3444
3630
  case "trace-log-set":
3445
3631
  return handleTraceLogSet(clientId, msg);
3446
3632
  case "trace-log-get":
@@ -3468,10 +3654,7 @@ function createDownstreamHandler(opts) {
3468
3654
  }
3469
3655
  return null;
3470
3656
  case "glasses_ui_nav_event":
3471
- // Client-reported nav-stack transition (push reports the PARENT
3472
- // surfaceId; pop reports the post-pop depth) so the plugin can
3473
- // pause/resume the right surface's cron. Not a debug tool — stays out
3474
- // of isExternalDebugToolMessageType.
3657
+
3475
3658
  if (typeof onGlassesUiNavEvent === "function") {
3476
3659
  try {
3477
3660
  onGlassesUiNavEvent({
@@ -3486,8 +3669,7 @@ function createDownstreamHandler(opts) {
3486
3669
  }
3487
3670
  return null;
3488
3671
  case "glasses_ui_render":
3489
- // Debug inject path used by tools/debugctl.js glasses-ui render to
3490
- // exercise the simulator paint loop without spinning up an agent.
3672
+
3491
3673
  if (typeof onGlassesUiRenderInject === "function") {
3492
3674
  try {
3493
3675
  onGlassesUiRenderInject({
@@ -3531,9 +3713,12 @@ function createDownstreamHandler(opts) {
3531
3713
  formatProtocol,
3532
3714
  formatStreaming,
3533
3715
  formatSessions,
3716
+ formatSessionDiff,
3717
+ sessionInfoFingerprint,
3534
3718
  formatSessionSwitched,
3535
3719
  formatModelsCatalog,
3536
3720
  formatSkillsCatalog,
3721
+ formatAgentsCatalog,
3537
3722
  formatSonioxModels,
3538
3723
  formatProviderUsageSnapshot,
3539
3724
  formatSessionModelConfig,
@@ -3564,21 +3749,10 @@ function createDownstreamHandler(opts) {
3564
3749
  formatReadinessProbeAck,
3565
3750
  formatError,
3566
3751
 
3567
- /**
3568
- * Check whether a client is subscribed to protocol frame forwarding.
3569
- *
3570
- * @param {string} clientId
3571
- * @returns {boolean}
3572
- */
3573
3752
  isProtocolSubscriber(clientId) {
3574
3753
  return protocolSubscribers.has(clientId);
3575
3754
  },
3576
3755
 
3577
- /**
3578
- * Clean up state for a disconnected client.
3579
- *
3580
- * @param {string} clientId
3581
- */
3582
3756
  removeClient(clientId) {
3583
3757
  protocolSubscribers.delete(clientId);
3584
3758
  },