ocuclaw 0.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +63 -8
  2. package/dist/config/runtime-config.js +81 -3
  3. package/dist/domain/activity-status-adapter.js +138 -605
  4. package/dist/domain/activity-status-arbiter.js +109 -0
  5. package/dist/domain/activity-status-labels.js +906 -0
  6. package/dist/domain/code-span-regions.js +103 -0
  7. package/dist/domain/conversation-state.js +14 -1
  8. package/dist/domain/debug-store.js +41 -184
  9. package/dist/domain/glasses-ui-content-summary.js +62 -0
  10. package/dist/domain/glasses-ui-system-prompt.js +28 -0
  11. package/dist/domain/message-emoji-allowlist.js +16 -0
  12. package/dist/domain/message-emoji-filter.js +33 -55
  13. package/dist/domain/neural-emoji-reactor-system-prompt.js +43 -0
  14. package/dist/domain/neural-emoji-reactor-tag-config.js +56 -0
  15. package/dist/domain/neural-pace-modulator-system-prompt.js +32 -0
  16. package/dist/domain/neural-pace-modulator-tag-config.js +51 -0
  17. package/dist/domain/tagged-span-parser.js +121 -0
  18. package/dist/domain/tagged-span-strip.js +38 -0
  19. package/dist/even-ai/even-ai-endpoint.js +91 -0
  20. package/dist/even-ai/even-ai-run-waiter.js +14 -0
  21. package/dist/even-ai/even-ai-settings-store.js +14 -0
  22. package/dist/gateway/gateway-bridge.js +14 -2
  23. package/dist/gateway/gateway-timing-ledger.js +457 -0
  24. package/dist/gateway/openclaw-client.js +462 -38
  25. package/dist/index.js +28 -1
  26. package/dist/runtime/downstream-handler.js +909 -68
  27. package/dist/runtime/downstream-server.js +1004 -512
  28. package/dist/runtime/ocuclaw-settings-store.js +74 -31
  29. package/dist/runtime/plugin-update-service.js +216 -0
  30. package/dist/runtime/protocol-adapter.js +9 -0
  31. package/dist/runtime/provider-usage-select.js +168 -0
  32. package/dist/runtime/relay-client-nudge-controller.js +553 -0
  33. package/dist/runtime/relay-core.js +1357 -210
  34. package/dist/runtime/relay-health-monitor.js +172 -0
  35. package/dist/runtime/relay-operation-registry.js +263 -0
  36. package/dist/runtime/relay-service.js +201 -1
  37. package/dist/runtime/relay-worker-approval-replay-cache.js +68 -0
  38. package/dist/runtime/relay-worker-entry.js +32 -0
  39. package/dist/runtime/relay-worker-health.js +272 -0
  40. package/dist/runtime/relay-worker-protocol.js +285 -0
  41. package/dist/runtime/relay-worker-queue.js +202 -0
  42. package/dist/runtime/relay-worker-supervisor.js +1081 -0
  43. package/dist/runtime/relay-worker-transport.js +1051 -0
  44. package/dist/runtime/session-context-service.js +189 -0
  45. package/dist/runtime/session-service.js +656 -38
  46. package/dist/runtime/upstream-runtime.js +1167 -60
  47. package/dist/tools/device-info-tool.js +242 -0
  48. package/dist/tools/glasses-ui-cron.js +427 -0
  49. package/dist/tools/glasses-ui-descriptors.js +261 -0
  50. package/dist/tools/glasses-ui-limits.js +21 -0
  51. package/dist/tools/glasses-ui-paint-floor.js +99 -0
  52. package/dist/tools/glasses-ui-recipes.js +746 -0
  53. package/dist/tools/glasses-ui-surfaces.js +278 -0
  54. package/dist/tools/glasses-ui-template.js +182 -0
  55. package/dist/tools/glasses-ui-tool.js +1147 -0
  56. package/dist/tools/session-title-tool.js +209 -0
  57. package/dist/version.js +2 -0
  58. package/openclaw.plugin.json +163 -15
  59. package/package.json +12 -4
@@ -4,6 +4,10 @@ import {
4
4
  normalizeOcuClawDefaultThinking,
5
5
  normalizeOcuClawSystemPrompt,
6
6
  } from "./ocuclaw-settings-store.js";
7
+ import {
8
+ formatMainOperationReceived,
9
+ formatSendAck,
10
+ } from "./relay-worker-protocol.js";
7
11
 
8
12
  // --- Factory ---
9
13
 
@@ -27,7 +31,7 @@ function normalizeLogger(logger) {
27
31
  * downstream clients (Even App, commander.html). Consumed by relay.js.
28
32
  *
29
33
  * @param {object} opts
30
- * @param {(id: string, text: string, sessionKey: string|null, attachment: object|null) => Promise} opts.onSend
34
+ * @param {(id: string, text: string, sessionKey: string|null, attachment: object|null, clientDisplaySignals: object|null) => Promise} opts.onSend
31
35
  * Forward a user message to the upstream OpenClaw agent.
32
36
  * @param {(sender: string, text: string) => Array} opts.onSimulate
33
37
  * Inject a fake message into conversation state; returns pages array.
@@ -41,10 +45,14 @@ function normalizeLogger(logger) {
41
45
  * Return cached skills catalog snapshot.
42
46
  * @param {() => Promise<{models: Array, fetchedAtMs: number, stale: boolean}>} [opts.onGetSonioxModels]
43
47
  * Return cached Soniox model snapshot.
48
+ * @param {() => Promise<object>} [opts.onGetProviderUsageSnapshot]
49
+ * Return the current provider usage snapshot for the active provider.
44
50
  * @param {() => Promise<object>} [opts.onGetSessionModelConfig]
45
51
  * Return current session model controls.
46
52
  * @param {(patch: object) => Promise<{status: string, error?: string}>} [opts.onSetSessionModelConfig]
47
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.
48
56
  * @param {() => Promise<object>} [opts.onGetEvenAiSettings]
49
57
  * Return current relay-owned Even AI settings.
50
58
  * @param {() => Promise<{sessions: Array, dedicatedKey: string}>} [opts.onGetEvenAiSessions]
@@ -63,6 +71,8 @@ function normalizeLogger(logger) {
63
71
  * Returns true if the OpenClaw gateway connection is active.
64
72
  * @param {(clientId: string, payload: object) => void} [opts.onEventDebug]
65
73
  * Optional structured client debug-event callback.
74
+ * @param {(clientId: string, payload: object) => Promise<object>|object} [opts.onReadinessProbe]
75
+ * Optional dedicated readiness-probe dispatcher.
66
76
  * @returns {object} Handler instance
67
77
  */
68
78
  function createDownstreamHandler(opts) {
@@ -79,8 +89,10 @@ function createDownstreamHandler(opts) {
79
89
  const onGetModelsCatalog = opts.onGetModelsCatalog;
80
90
  const onGetSkillsCatalog = opts.onGetSkillsCatalog;
81
91
  const onGetSonioxModels = opts.onGetSonioxModels || null;
92
+ const onGetProviderUsageSnapshot = opts.onGetProviderUsageSnapshot || null;
82
93
  const onGetSessionModelConfig = opts.onGetSessionModelConfig;
83
94
  const onSetSessionModelConfig = opts.onSetSessionModelConfig;
95
+ const onCompactSession = opts.onCompactSession || null;
84
96
  const onGetEvenAiSettings = opts.onGetEvenAiSettings;
85
97
  const onGetEvenAiSessions = opts.onGetEvenAiSessions;
86
98
  const onSetEvenAiSettings = opts.onSetEvenAiSettings;
@@ -94,8 +106,21 @@ function createDownstreamHandler(opts) {
94
106
  const onDebugSet = opts.onDebugSet || null;
95
107
  const onDebugDump = opts.onDebugDump || null;
96
108
  const onEventDebug = opts.onEventDebug || null;
109
+ const onTraceLogSet = opts.onTraceLogSet || null;
110
+ const onTraceLogGet = opts.onTraceLogGet || null;
97
111
  const onRemoteControl = opts.onRemoteControl || null;
112
+ const onAutomationState = opts.onAutomationState || null;
113
+ const onReadinessProbe = opts.onReadinessProbe || null;
114
+ const onGlassesUiResult = opts.onGlassesUiResult || null;
115
+ const onGlassesUiRenderInject = opts.onGlassesUiRenderInject || null;
116
+ const onGlassesUiNavEvent = opts.onGlassesUiNavEvent || null;
117
+ const onDeviceInfoResponse = opts.onDeviceInfoResponse || null;
118
+ const onSetUserSessionTitle = opts.onSetUserSessionTitle || null;
119
+ const onSetSessionPinned = opts.onSetSessionPinned || null;
120
+ const onDeleteSessions = opts.onDeleteSessions || null;
121
+ const onSearchTranscripts = opts.onSearchTranscripts || null;
98
122
  const getSnapshotRevision = opts.getSnapshotRevision || null;
123
+ const operationRegistry = opts.operationRegistry || null;
99
124
 
100
125
  /** Client IDs subscribed to raw protocol frame forwarding. */
101
126
  const protocolSubscribers = new Set();
@@ -112,6 +137,8 @@ function createDownstreamHandler(opts) {
112
137
  const approvalResolveCache = new Map();
113
138
  const APP_PROTOCOL = {
114
139
  activity: "ocuclaw.activity.update",
140
+ automationStateGet: "ocuclaw.automation.state.get",
141
+ automationStateSnapshot: "ocuclaw.automation.state.snapshot",
115
142
  approvalRequest: "ocuclaw.approval.request",
116
143
  approvalResolve: "ocuclaw.approval.resolve",
117
144
  approvalResolveAck: "ocuclaw.approval.resolve.ack",
@@ -134,11 +161,15 @@ function createDownstreamHandler(opts) {
134
161
  messageStreamDelta: "ocuclaw.message.stream.delta",
135
162
  modelCatalogGet: "ocuclaw.model.catalog.get",
136
163
  modelCatalogSnapshot: "ocuclaw.model.catalog.snapshot",
164
+ providerUsageGet: "ocuclaw.provider.usage.get",
165
+ providerUsageSnapshot: "ocuclaw.provider.usage.snapshot",
137
166
  skillsCatalogGet: "ocuclaw.skills.catalog.get",
138
167
  skillsCatalogSnapshot: "ocuclaw.skills.catalog.snapshot",
139
168
  pages: "ocuclaw.view.pages.snapshot",
140
169
  protocolSubscribe: "ocuclaw.protocol.tap.subscribe",
141
170
  protocolFrame: "ocuclaw.protocol.tap.frame",
171
+ readinessProbeAck: "ocuclaw.readiness.probe.ack",
172
+ readinessProbeRequest: "ocuclaw.readiness.probe.request",
142
173
  remoteControl: "ocuclaw.remote.control",
143
174
  requestSonioxTemporaryKey: "requestSonioxTemporaryKey",
144
175
  sonioxModelsGet: "ocuclaw.voice.soniox.models.get",
@@ -147,16 +178,20 @@ function createDownstreamHandler(opts) {
147
178
  sessionConfigSet: "ocuclaw.session.config.set",
148
179
  sessionConfigSetAck: "ocuclaw.session.config.set.ack",
149
180
  sessionConfigSnapshot: "ocuclaw.session.config.snapshot",
181
+ sessionCompact: "ocuclaw.session.compact",
182
+ sessionCompactAck: "ocuclaw.session.compact.ack",
150
183
  sessionCreate: "ocuclaw.session.create",
151
184
  sessionList: "ocuclaw.session.list",
152
185
  sessionListResult: "ocuclaw.session.list.result",
153
186
  sessionReset: "ocuclaw.session.reset",
154
187
  sessionSwitch: "ocuclaw.session.switch",
155
188
  sessionSwitchApplied: "ocuclaw.session.switch.applied",
189
+ sessionTitleSet: "ocuclaw.session.title.set",
156
190
  sonioxTemporaryKey: "sonioxTemporaryKey",
157
191
  sonioxTemporaryKeyError: "sonioxTemporaryKeyError",
158
192
  status: "ocuclaw.runtime.status",
159
193
  statusGet: "ocuclaw.runtime.status.get",
194
+ typingUpdate: "ocuclaw.typing.update",
160
195
  };
161
196
 
162
197
  // --- Format helpers ---
@@ -217,24 +252,52 @@ function createDownstreamHandler(opts) {
217
252
  return JSON.stringify({ ...activity, type: APP_PROTOCOL.activity });
218
253
  }
219
254
 
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
+ function formatTyping(update) {
262
+ return JSON.stringify({ ...update, type: APP_PROTOCOL.typingUpdate });
263
+ }
264
+
220
265
  /**
221
266
  * Format an error message for unicast.
222
267
  *
223
268
  * @param {string} error
269
+ * @param {{code?: string, requestId?: string, op?: string}} [meta]
224
270
  * @returns {string} JSON string
225
271
  */
226
- function formatError(error) {
227
- return JSON.stringify({
272
+ function formatError(error, meta) {
273
+ const msg = {
228
274
  type: "error",
229
275
  error: error || "Unknown error",
230
- });
276
+ };
277
+ if (meta && typeof meta === "object" && !Array.isArray(meta)) {
278
+ if (typeof meta.code === "string" && meta.code.trim()) {
279
+ msg.code = meta.code.trim();
280
+ }
281
+ if (typeof meta.requestId === "string" && meta.requestId.trim()) {
282
+ msg.requestId = meta.requestId.trim();
283
+ }
284
+ if (typeof meta.op === "string" && meta.op.trim()) {
285
+ msg.op = meta.op.trim();
286
+ }
287
+ }
288
+ return JSON.stringify(msg);
231
289
  }
232
290
 
233
291
  function isExternalDebugToolMessageType(messageType) {
234
292
  return (
235
293
  messageType === "debug-set" ||
236
294
  messageType === "debug-dump" ||
237
- messageType === "remote-control"
295
+ messageType === "trace-log-set" ||
296
+ messageType === "trace-log-get" ||
297
+ messageType === "remote-control" ||
298
+ messageType === APP_PROTOCOL.automationStateGet ||
299
+ messageType === APP_PROTOCOL.readinessProbeRequest ||
300
+ messageType === "glasses_ui_render"
238
301
  );
239
302
  }
240
303
 
@@ -247,15 +310,12 @@ function createDownstreamHandler(opts) {
247
310
  * @param {string} [errorCode] - Structured error code (for deterministic client handling)
248
311
  * @returns {string} JSON string
249
312
  */
250
- function formatSendAck(id, status, error, errorCode) {
251
- const msg = { type: APP_PROTOCOL.messageSendAck, requestId: id, status };
252
- if (error !== undefined) {
253
- msg.error = error;
254
- }
255
- if (errorCode !== undefined) {
256
- msg.errorCode = errorCode;
257
- }
258
- return JSON.stringify(msg);
313
+ function formatSendAckCompat(id, status, error, errorCode) {
314
+ return formatSendAck(id, status, error, errorCode);
315
+ }
316
+
317
+ function formatOperationReceived(data) {
318
+ return formatMainOperationReceived(data);
259
319
  }
260
320
 
261
321
  /**
@@ -277,10 +337,18 @@ function createDownstreamHandler(opts) {
277
337
  * Format a streaming text message for broadcast during agent runs.
278
338
  *
279
339
  * @param {string} text - Streaming assistant text
340
+ * @param {Array<{start: number, end: number, emoji: string}>} [spans] - Optional emoji spans
280
341
  * @returns {string} JSON string
281
342
  */
282
- function formatStreaming(text) {
283
- return JSON.stringify({ type: APP_PROTOCOL.messageStreamDelta, text });
343
+ function formatStreaming(text, emojiSpans, paceSpans) {
344
+ const payload = { type: APP_PROTOCOL.messageStreamDelta, text };
345
+ if (Array.isArray(emojiSpans) && emojiSpans.length > 0) {
346
+ payload.emojiSpans = emojiSpans;
347
+ }
348
+ if (Array.isArray(paceSpans) && paceSpans.length > 0) {
349
+ payload.paceSpans = paceSpans;
350
+ }
351
+ return JSON.stringify(payload);
284
352
  }
285
353
 
286
354
  /**
@@ -360,6 +428,75 @@ function createDownstreamHandler(opts) {
360
428
  });
361
429
  }
362
430
 
431
+ /**
432
+ * Format a provider usage snapshot for unicast.
433
+ *
434
+ * @param {object} payload
435
+ * @returns {string}
436
+ */
437
+ function formatProviderUsageSnapshot(payload) {
438
+ const provider =
439
+ payload && typeof payload.provider === "string" && payload.provider.trim()
440
+ ? payload.provider.trim()
441
+ : null;
442
+ const windows = Array.isArray(payload && payload.windows)
443
+ ? payload.windows.map((window) => ({
444
+ key:
445
+ window && typeof window.key === "string" && window.key.trim()
446
+ ? window.key.trim()
447
+ : null,
448
+ label:
449
+ window && typeof window.label === "string" && window.label.trim()
450
+ ? window.label.trim()
451
+ : null,
452
+ usedPercent:
453
+ Number.isFinite(window && window.usedPercent)
454
+ ? window.usedPercent
455
+ : 0,
456
+ resetAtMs:
457
+ Number.isFinite(window && window.resetAtMs)
458
+ ? Math.floor(window.resetAtMs)
459
+ : null,
460
+ sortOrder:
461
+ Number.isFinite(window && window.sortOrder)
462
+ ? Math.floor(window.sortOrder)
463
+ : null,
464
+ }))
465
+ : [];
466
+ return JSON.stringify({
467
+ type: APP_PROTOCOL.providerUsageSnapshot,
468
+ sessionKey:
469
+ payload && typeof payload.sessionKey === "string" && payload.sessionKey.trim()
470
+ ? payload.sessionKey.trim()
471
+ : null,
472
+ provider,
473
+ displayName:
474
+ payload && typeof payload.displayName === "string" && payload.displayName.trim()
475
+ ? payload.displayName.trim()
476
+ : provider,
477
+ limitingWindowKey:
478
+ payload &&
479
+ typeof payload.limitingWindowKey === "string" &&
480
+ payload.limitingWindowKey.trim()
481
+ ? payload.limitingWindowKey.trim()
482
+ : null,
483
+ windows,
484
+ fetchedAtMs:
485
+ Number.isFinite(payload && payload.fetchedAtMs)
486
+ ? Math.floor(payload.fetchedAtMs)
487
+ : Date.now(),
488
+ stale: !!(payload && payload.stale),
489
+ poolStatus:
490
+ payload && (payload.poolStatus === "ready" || payload.poolStatus === "exhausted")
491
+ ? payload.poolStatus
492
+ : "unknown",
493
+ totalProfileCount:
494
+ Number.isFinite(payload && payload.totalProfileCount) && payload.totalProfileCount >= 0
495
+ ? Math.floor(payload.totalProfileCount)
496
+ : null,
497
+ });
498
+ }
499
+
363
500
  /**
364
501
  * Format current session model controls.
365
502
  *
@@ -388,6 +525,11 @@ function createDownstreamHandler(opts) {
388
525
  payload && typeof payload.verboseLevel === "string"
389
526
  ? payload.verboseLevel
390
527
  : "off",
528
+ fastMode: !!(payload && payload.fastMode === true),
529
+ elevatedLevel:
530
+ payload && typeof payload.elevatedLevel === "string"
531
+ ? payload.elevatedLevel
532
+ : "off",
391
533
  });
392
534
  }
393
535
 
@@ -411,6 +553,30 @@ function createDownstreamHandler(opts) {
411
553
  return JSON.stringify(out);
412
554
  }
413
555
 
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
+ function formatCompactSessionAck(payload) {
563
+ const msg = {
564
+ type: APP_PROTOCOL.sessionCompactAck,
565
+ status:
566
+ payload && payload.status === "accepted" ? "accepted" : "rejected",
567
+ };
568
+ if (payload && payload.requestId) {
569
+ msg.requestId = String(payload.requestId);
570
+ }
571
+ if (msg.status === "rejected") {
572
+ msg.error =
573
+ payload && payload.error
574
+ ? String(payload.error)
575
+ : "compact failed";
576
+ }
577
+ return JSON.stringify(msg);
578
+ }
579
+
414
580
  /**
415
581
  * Format the current relay-owned Even AI settings snapshot.
416
582
  *
@@ -438,6 +604,7 @@ function createDownstreamHandler(opts) {
438
604
  ? payload.defaultThinking
439
605
  : "",
440
606
  listenEnabled: payload && payload.listenEnabled === true,
607
+ defaultFastMode: !!(payload && payload.defaultFastMode === true),
441
608
  });
442
609
  }
443
610
 
@@ -485,6 +652,7 @@ function createDownstreamHandler(opts) {
485
652
  ? payload.defaultThinking
486
653
  : undefined,
487
654
  ),
655
+ defaultFastMode: !!(payload && payload.defaultFastMode === true),
488
656
  });
489
657
  }
490
658
 
@@ -533,23 +701,52 @@ function createDownstreamHandler(opts) {
533
701
  */
534
702
  function formatApproval(data) {
535
703
  const request = data && data.request ? data.request : {};
704
+ const approvalKind =
705
+ (data && data.approvalKind === "plugin") ||
706
+ (data && typeof data.id === "string" && data.id.startsWith("plugin:")) ||
707
+ (typeof request.title === "string" && request.title.length > 0)
708
+ ? "plugin"
709
+ : "exec";
710
+ const isPluginApproval = approvalKind === "plugin";
711
+ const commandText =
712
+ request.command ||
713
+ (request.host === "node" && request.systemRunPlan && typeof request.systemRunPlan.commandText === "string"
714
+ ? request.systemRunPlan.commandText
715
+ : "") ||
716
+ (isPluginApproval && typeof request.title === "string" ? request.title : "") ||
717
+ "";
718
+ const pluginDescription =
719
+ isPluginApproval && typeof request.description === "string" && request.description.length > 0
720
+ ? request.description
721
+ : null;
536
722
  return JSON.stringify({
537
723
  type: APP_PROTOCOL.approvalRequest,
538
724
  id: data.id,
725
+ ...(isPluginApproval ? { approvalKind: "plugin" } : {}),
539
726
  requestId:
540
727
  (data && typeof data.requestId === "string" && data.requestId) ||
541
728
  (request && typeof request.requestId === "string" && request.requestId) ||
542
729
  null,
543
- command: request.command || "",
544
- cwd: request.cwd || null,
730
+ command: commandText,
731
+ cwd: request.cwd || pluginDescription || null,
545
732
  agentId: request.agentId || null,
546
- host: request.host || null,
547
- security: request.security || null,
733
+ host: request.host || (isPluginApproval ? "plugin" : null),
734
+ security: request.security || (isPluginApproval && typeof request.severity === "string" ? request.severity : null),
548
735
  ask: request.ask || null,
549
736
  resolvedPath: request.resolvedPath || null,
550
737
  sessionKey: request.sessionKey || null,
738
+ ...(isPluginApproval
739
+ ? {
740
+ pluginId: typeof request.pluginId === "string" ? request.pluginId : null,
741
+ toolName: typeof request.toolName === "string" ? request.toolName : null,
742
+ description: pluginDescription,
743
+ }
744
+ : {}),
551
745
  createdAtMs: data.createdAtMs || 0,
552
746
  expiresAtMs: data.expiresAtMs || 0,
747
+ allowedDecisions: Array.isArray(request.allowedDecisions)
748
+ ? request.allowedDecisions.filter((d) => typeof d === "string")
749
+ : null,
553
750
  });
554
751
  }
555
752
 
@@ -605,17 +802,6 @@ function createDownstreamHandler(opts) {
605
802
  });
606
803
  }
607
804
 
608
- /**
609
- * Format a transcription update for broadcast during voice mode.
610
- *
611
- * @param {string} text - Current transcription text
612
- * @param {boolean} isFinal - Whether this is a final transcription
613
- * @returns {string} JSON string
614
- */
615
- function formatTranscription(text, isFinal) {
616
- return JSON.stringify({ type: "transcription", text, final: isFinal });
617
- }
618
-
619
805
  /**
620
806
  * Format a committed listen handoff update for broadcast during voice mode.
621
807
  *
@@ -633,6 +819,13 @@ function createDownstreamHandler(opts) {
633
819
  });
634
820
  }
635
821
 
822
+ function formatEvenAiListenIntercepted(sessionKey) {
823
+ return JSON.stringify({
824
+ type: "even-ai-listen-intercepted",
825
+ sessionKey: sessionKey ?? null,
826
+ });
827
+ }
828
+
636
829
  /**
637
830
  * Format a listen-ended message for broadcast.
638
831
  * @returns {string} JSON string
@@ -722,6 +915,10 @@ function createDownstreamHandler(opts) {
722
915
  : "";
723
916
  const lowered = message.toLowerCase();
724
917
  if (!message) return "soniox_temp_key_request_failed";
918
+ // AbortError from the per-fetch timeout AbortController in mintSonioxTemporaryKey.
919
+ if (err && err.name === "AbortError") {
920
+ return "soniox_temp_key_mint_timeout";
921
+ }
725
922
  if (lowered.includes("is not available")) {
726
923
  return "soniox_temp_key_unavailable";
727
924
  }
@@ -770,6 +967,27 @@ function createDownstreamHandler(opts) {
770
967
  });
771
968
  }
772
969
 
970
+ /**
971
+ * Parse a "trace-log-set" message.
972
+ * @returns {{enabled: boolean}}
973
+ */
974
+ function parseTraceLogSet(msg) {
975
+ if (!msg || typeof msg !== "object") {
976
+ throw new Error("trace-log-set requires an object");
977
+ }
978
+ if (typeof msg.enabled !== "boolean") {
979
+ throw new Error("trace-log-set requires boolean 'enabled'");
980
+ }
981
+ return { enabled: msg.enabled };
982
+ }
983
+
984
+ /**
985
+ * Format a trace-log response for unicast (set + get share one type).
986
+ */
987
+ function formatTraceLog(data) {
988
+ return JSON.stringify({ type: "trace-log", ...data });
989
+ }
990
+
773
991
  /**
774
992
  * Format the current relay debug-config snapshot for app/WebUI clients.
775
993
  *
@@ -827,6 +1045,98 @@ function createDownstreamHandler(opts) {
827
1045
  });
828
1046
  }
829
1047
 
1048
+ function formatAutomationStateRequest(data) {
1049
+ return JSON.stringify({
1050
+ type: APP_PROTOCOL.automationStateGet,
1051
+ requestId:
1052
+ data && typeof data.requestId === "string" && data.requestId
1053
+ ? data.requestId
1054
+ : null,
1055
+ sessionKey:
1056
+ data && typeof data.sessionKey === "string" && data.sessionKey
1057
+ ? data.sessionKey
1058
+ : null,
1059
+ });
1060
+ }
1061
+
1062
+ function formatAutomationStateSnapshot(data) {
1063
+ return JSON.stringify({
1064
+ type: APP_PROTOCOL.automationStateSnapshot,
1065
+ ok: data && data.ok !== false,
1066
+ requestId:
1067
+ data && typeof data.requestId === "string" && data.requestId
1068
+ ? data.requestId
1069
+ : null,
1070
+ state:
1071
+ data && data.state && typeof data.state === "object" ? data.state : null,
1072
+ reasonCode:
1073
+ data && typeof data.reasonCode === "string" && data.reasonCode
1074
+ ? data.reasonCode
1075
+ : null,
1076
+ message:
1077
+ data && typeof data.message === "string" && data.message
1078
+ ? data.message
1079
+ : null,
1080
+ });
1081
+ }
1082
+
1083
+ function formatReadinessProbeRequest(data) {
1084
+ return JSON.stringify({
1085
+ type: APP_PROTOCOL.readinessProbeRequest,
1086
+ requestId:
1087
+ data && typeof data.requestId === "string" && data.requestId
1088
+ ? data.requestId
1089
+ : null,
1090
+ sinceMs:
1091
+ data && Number.isFinite(Number(data.sinceMs))
1092
+ ? Math.max(0, Math.floor(Number(data.sinceMs)))
1093
+ : 0,
1094
+ sessionKey:
1095
+ data && typeof data.sessionKey === "string" && data.sessionKey
1096
+ ? data.sessionKey
1097
+ : null,
1098
+ });
1099
+ }
1100
+
1101
+ function formatReadinessProbeAck(data) {
1102
+ return JSON.stringify({
1103
+ type: APP_PROTOCOL.readinessProbeAck,
1104
+ ok: data && data.ok !== false,
1105
+ requestId:
1106
+ data && typeof data.requestId === "string" && data.requestId
1107
+ ? data.requestId
1108
+ : null,
1109
+ reasonCode:
1110
+ data && typeof data.reasonCode === "string" && data.reasonCode
1111
+ ? data.reasonCode
1112
+ : null,
1113
+ message:
1114
+ data && typeof data.message === "string" && data.message
1115
+ ? data.message
1116
+ : null,
1117
+ activeSessionKey:
1118
+ data && typeof data.activeSessionKey === "string" && data.activeSessionKey
1119
+ ? data.activeSessionKey
1120
+ : null,
1121
+ emittedAtMs:
1122
+ data && Number.isFinite(Number(data.emittedAtMs))
1123
+ ? Math.max(0, Math.floor(Number(data.emittedAtMs)))
1124
+ : null,
1125
+ clientId:
1126
+ data && typeof data.clientId === "string" && data.clientId
1127
+ ? data.clientId
1128
+ : null,
1129
+ clientName:
1130
+ data && typeof data.clientName === "string" && data.clientName
1131
+ ? data.clientName
1132
+ : null,
1133
+ clientVersion:
1134
+ data && typeof data.clientVersion === "string" && data.clientVersion
1135
+ ? data.clientVersion
1136
+ : null,
1137
+ });
1138
+ }
1139
+
830
1140
  function normalizeCategories(raw, fieldName) {
831
1141
  if (raw === undefined || raw === null) return [];
832
1142
  if (!Array.isArray(raw)) {
@@ -900,25 +1210,12 @@ function createDownstreamHandler(opts) {
900
1210
  ) {
901
1211
  throw new Error("debug-dump untilMs must be a non-negative number");
902
1212
  }
903
- if (msg.redaction !== undefined) {
904
- if (typeof msg.redaction !== "string") {
905
- throw new Error("debug-dump redaction must be one of: safe, full");
906
- }
907
- const normalized = msg.redaction.trim().toLowerCase();
908
- if (normalized !== "safe" && normalized !== "full") {
909
- throw new Error("debug-dump redaction must be one of: safe, full");
910
- }
911
- }
912
1213
  return {
913
1214
  categories,
914
1215
  limit: msg.limit === undefined ? undefined : Number(msg.limit),
915
1216
  sinceMs: msg.sinceMs === undefined ? undefined : Number(msg.sinceMs),
916
1217
  sinceAgeMs: msg.sinceAgeMs === undefined ? undefined : Number(msg.sinceAgeMs),
917
1218
  untilMs: msg.untilMs === undefined ? undefined : Number(msg.untilMs),
918
- redaction:
919
- msg.redaction === undefined
920
- ? undefined
921
- : msg.redaction.trim().toLowerCase(),
922
1219
  };
923
1220
  }
924
1221
 
@@ -1187,12 +1484,19 @@ function createDownstreamHandler(opts) {
1187
1484
 
1188
1485
  const payload = {};
1189
1486
 
1190
- const modelRef = parseOptionalTrimmedString(msg.modelRef);
1191
- if (modelRef) {
1192
- if (!modelRef.includes("/")) {
1193
- throw new Error("modelRef must be in provider/id format");
1487
+ if (Object.prototype.hasOwnProperty.call(msg, "modelRef")) {
1488
+ if (typeof msg.modelRef !== "string") {
1489
+ throw new Error("modelRef must be in provider/id format or blank");
1490
+ }
1491
+ const modelRef = msg.modelRef.trim();
1492
+ if (!modelRef) {
1493
+ payload.modelRef = "";
1494
+ } else {
1495
+ if (!modelRef.includes("/")) {
1496
+ throw new Error("modelRef must be in provider/id format");
1497
+ }
1498
+ payload.modelRef = modelRef;
1194
1499
  }
1195
- payload.modelRef = modelRef;
1196
1500
  }
1197
1501
 
1198
1502
  if (Object.prototype.hasOwnProperty.call(msg, "thinkingLevel")) {
@@ -1230,6 +1534,20 @@ function createDownstreamHandler(opts) {
1230
1534
  payload.verboseLevel = normalized;
1231
1535
  }
1232
1536
 
1537
+ const fastMode = parseOptionalBoolean(msg.fastMode, "fastMode");
1538
+ if (fastMode !== undefined) {
1539
+ payload.fastMode = fastMode;
1540
+ }
1541
+
1542
+ const elevatedLevel = parseOptionalTrimmedString(msg.elevatedLevel);
1543
+ if (elevatedLevel) {
1544
+ const normalized = elevatedLevel.toLowerCase();
1545
+ if (!["off", "on", "ask", "full"].includes(normalized)) {
1546
+ throw new Error("elevatedLevel must be off|on|ask|full");
1547
+ }
1548
+ payload.elevatedLevel = normalized;
1549
+ }
1550
+
1233
1551
  if (Object.keys(payload).length === 0) {
1234
1552
  throw new Error("setSessionModelConfig requires at least one field");
1235
1553
  }
@@ -1301,6 +1619,13 @@ function createDownstreamHandler(opts) {
1301
1619
  payload.listenEnabled = msg.listenEnabled;
1302
1620
  }
1303
1621
 
1622
+ if (Object.prototype.hasOwnProperty.call(msg, "defaultFastMode")) {
1623
+ if (typeof msg.defaultFastMode !== "boolean") {
1624
+ throw new Error("defaultFastMode must be a boolean");
1625
+ }
1626
+ payload.defaultFastMode = msg.defaultFastMode;
1627
+ }
1628
+
1304
1629
  if (Object.keys(payload).length === 0) {
1305
1630
  throw new Error("setEvenAiSettings requires at least one field");
1306
1631
  }
@@ -1336,6 +1661,13 @@ function createDownstreamHandler(opts) {
1336
1661
  payload.defaultThinking = normalizeOcuClawDefaultThinking(msg.defaultThinking);
1337
1662
  }
1338
1663
 
1664
+ if (Object.prototype.hasOwnProperty.call(msg, "defaultFastMode")) {
1665
+ if (typeof msg.defaultFastMode !== "boolean") {
1666
+ throw new Error("defaultFastMode must be a boolean");
1667
+ }
1668
+ payload.defaultFastMode = msg.defaultFastMode;
1669
+ }
1670
+
1339
1671
  if (Object.keys(payload).length === 0) {
1340
1672
  throw new Error("setOcuClawSettings requires at least one field");
1341
1673
  }
@@ -1428,6 +1760,20 @@ function createDownstreamHandler(opts) {
1428
1760
  return payload;
1429
1761
  }
1430
1762
 
1763
+ if (action === "setting-set") {
1764
+ const settingKey = parseOptionalTrimmedString(msg.settingKey);
1765
+ const value = parseOptionalTrimmedString(msg.value);
1766
+ if (!settingKey) {
1767
+ throw new Error("remote-control setting-set requires settingKey");
1768
+ }
1769
+ if (!value) {
1770
+ throw new Error("remote-control setting-set requires value");
1771
+ }
1772
+ payload.settingKey = settingKey;
1773
+ payload.value = value;
1774
+ return payload;
1775
+ }
1776
+
1431
1777
  if (action === "relay-action") {
1432
1778
  payload.relayAction = normalizeRemoteRelayAction(msg.relayAction);
1433
1779
  if (
@@ -1489,6 +1835,39 @@ function createDownstreamHandler(opts) {
1489
1835
  throw new Error(`unsupported remote-control action: ${actionRaw}`);
1490
1836
  }
1491
1837
 
1838
+ function parseReadinessProbe(msg) {
1839
+ if (!msg || typeof msg !== "object") {
1840
+ throw new Error("readiness probe payload must be an object");
1841
+ }
1842
+ const requestId = parseOptionalTrimmedString(msg.requestId);
1843
+ if (!requestId) {
1844
+ throw new Error("readiness probe requires requestId");
1845
+ }
1846
+ const sinceMs = parseOptionalNonNegativeNumber(msg.sinceMs, "sinceMs");
1847
+ if (sinceMs === undefined) {
1848
+ throw new Error("readiness probe sinceMs must be a non-negative number");
1849
+ }
1850
+ return {
1851
+ requestId,
1852
+ sinceMs,
1853
+ sessionKey: parseOptionalTrimmedString(msg.sessionKey) || null,
1854
+ };
1855
+ }
1856
+
1857
+ function parseAutomationStateGet(msg) {
1858
+ if (!msg || typeof msg !== "object") {
1859
+ throw new Error("automation state payload must be an object");
1860
+ }
1861
+ const requestId = parseOptionalTrimmedString(msg.requestId);
1862
+ if (!requestId) {
1863
+ throw new Error("automation state request requires requestId");
1864
+ }
1865
+ return {
1866
+ requestId,
1867
+ sessionKey: parseOptionalTrimmedString(msg.sessionKey) || null,
1868
+ };
1869
+ }
1870
+
1492
1871
  const ATTACHMENT_MAX_DECODED_BYTES = 5_000_000;
1493
1872
  const ATTACHMENT_MAX_ENCODED_CHARS =
1494
1873
  Math.ceil((ATTACHMENT_MAX_DECODED_BYTES * 4) / 3) + 16;
@@ -1627,6 +2006,32 @@ function createDownstreamHandler(opts) {
1627
2006
  };
1628
2007
  }
1629
2008
 
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
+ function parseClientDisplaySignals(raw) {
2015
+ if (raw == null || typeof raw !== "object") return null;
2016
+ const coerceState = (val) => {
2017
+ const s = typeof val === "string" ? val : null;
2018
+ return s === "active" || s === "recently-disabled" || s === "inactive"
2019
+ ? s
2020
+ : "inactive";
2021
+ };
2022
+ const state = coerceState(raw.neuralEmojiReactorState);
2023
+ const paceState = coerceState(raw.neuralPaceModulatorState);
2024
+ const enabledRaw = raw.neuralSessionNamesEnabled;
2025
+ const neuralSessionNamesEnabled =
2026
+ typeof enabledRaw === "boolean" ? enabledRaw : true;
2027
+ // readSpeedWpm tolerated but ignored on inbound for old clients.
2028
+ return {
2029
+ neuralEmojiReactorState: state,
2030
+ neuralPaceModulatorState: paceState,
2031
+ neuralSessionNamesEnabled,
2032
+ };
2033
+ }
2034
+
1630
2035
  // --- Message handlers ---
1631
2036
 
1632
2037
  /**
@@ -1640,7 +2045,7 @@ function createDownstreamHandler(opts) {
1640
2045
  const requestId = parseOptionalTrimmedString(msg.requestId);
1641
2046
  if (!requestId) {
1642
2047
  return {
1643
- unicast: formatSendAck(
2048
+ unicast: formatSendAckCompat(
1644
2049
  requestId,
1645
2050
  "rejected",
1646
2051
  "Missing required field: requestId",
@@ -1651,7 +2056,7 @@ function createDownstreamHandler(opts) {
1651
2056
  const parsedAttachment = parseAttachment(msg.attachment);
1652
2057
  if (!parsedAttachment.ok) {
1653
2058
  return {
1654
- unicast: formatSendAck(
2059
+ unicast: formatSendAckCompat(
1655
2060
  requestId,
1656
2061
  "rejected",
1657
2062
  parsedAttachment.error,
@@ -1663,7 +2068,7 @@ function createDownstreamHandler(opts) {
1663
2068
  const text = typeof msg.text === "string" ? msg.text : "";
1664
2069
  if (!text.trim() && !parsedAttachment.attachment) {
1665
2070
  return {
1666
- unicast: formatSendAck(
2071
+ unicast: formatSendAckCompat(
1667
2072
  requestId,
1668
2073
  "rejected",
1669
2074
  "Missing required field: text",
@@ -1674,7 +2079,7 @@ function createDownstreamHandler(opts) {
1674
2079
  // Check upstream connectivity
1675
2080
  if (!isUpstreamConnected()) {
1676
2081
  return {
1677
- unicast: formatSendAck(
2082
+ unicast: formatSendAckCompat(
1678
2083
  requestId,
1679
2084
  "rejected",
1680
2085
  "OpenClaw disconnected",
@@ -1682,26 +2087,66 @@ function createDownstreamHandler(opts) {
1682
2087
  };
1683
2088
  }
1684
2089
 
2090
+ const operation =
2091
+ operationRegistry && typeof operationRegistry.beginMessageSend === "function"
2092
+ ? operationRegistry.beginMessageSend({
2093
+ requestId,
2094
+ clientId,
2095
+ sessionKey: msg.sessionKey || null,
2096
+ })
2097
+ : null;
2098
+
2099
+ if (operation && operation.duplicate) {
2100
+ const frames = operation.finalFrame
2101
+ ? [operation.receipt, operation.finalFrame]
2102
+ : [operation.receipt];
2103
+ return { unicast: frames };
2104
+ }
2105
+
2106
+ const clientDisplaySignals = parseClientDisplaySignals(msg.clientDisplaySignals);
2107
+
1685
2108
  // Forward to upstream — resolves on initial ack (accepted/queued)
1686
- return onSend(
2109
+ const followup = onSend(
1687
2110
  requestId,
1688
2111
  text,
1689
2112
  msg.sessionKey || null,
1690
2113
  parsedAttachment.attachment,
2114
+ clientDisplaySignals,
1691
2115
  ).then(
1692
2116
  (result) => {
1693
2117
  const status = (result && result.status) || "accepted";
1694
- return { unicast: formatSendAck(requestId, status) };
2118
+ const frame = formatSendAckCompat(requestId, status);
2119
+ if (operation && typeof operation.complete === "function") {
2120
+ operation.complete(frame, { status });
2121
+ }
2122
+ return { unicast: frame };
1695
2123
  },
1696
- (err) => ({
1697
- unicast: formatSendAck(
2124
+ (err) => {
2125
+ const frame = formatSendAckCompat(
1698
2126
  requestId,
1699
2127
  "rejected",
1700
2128
  err.message || "Send failed",
1701
- err.errorCode || undefined,
1702
- ),
1703
- }),
2129
+ err.errorCode || err.code || undefined,
2130
+ );
2131
+ if (operation && typeof operation.fail === "function") {
2132
+ operation.fail(frame, {
2133
+ errorCode: err.errorCode || err.code || null,
2134
+ message: err.message || "Send failed",
2135
+ });
2136
+ }
2137
+ return { unicast: frame };
2138
+ },
1704
2139
  );
2140
+
2141
+ return operation
2142
+ ? {
2143
+ unicast: operation.receipt || formatOperationReceived({
2144
+ requestId,
2145
+ operation: "message.send",
2146
+ }),
2147
+ followup,
2148
+ }
2149
+ : followup;
1705
2150
  }
1706
2151
 
1707
2152
  /**
@@ -1730,7 +2175,7 @@ function createDownstreamHandler(opts) {
1730
2175
  const id = parseOptionalTrimmedString(msg.id);
1731
2176
  if (!id) {
1732
2177
  return {
1733
- unicast: formatSendAck(
2178
+ unicast: formatSendAckCompat(
1734
2179
  msg.id || null,
1735
2180
  "rejected",
1736
2181
  "Missing required field: id",
@@ -1740,7 +2185,7 @@ function createDownstreamHandler(opts) {
1740
2185
 
1741
2186
  if (!onSimulateStream) {
1742
2187
  return {
1743
- unicast: formatSendAck(
2188
+ unicast: formatSendAckCompat(
1744
2189
  id,
1745
2190
  "rejected",
1746
2191
  "simulateStream not supported by relay",
@@ -1751,7 +2196,7 @@ function createDownstreamHandler(opts) {
1751
2196
  const text = typeof msg.text === "string" ? msg.text : "";
1752
2197
  if (!text.trim()) {
1753
2198
  return {
1754
- unicast: formatSendAck(
2199
+ unicast: formatSendAckCompat(
1755
2200
  id,
1756
2201
  "rejected",
1757
2202
  "simulateStream requires non-empty text",
@@ -1770,7 +2215,7 @@ function createDownstreamHandler(opts) {
1770
2215
  thinkingTailMs = parseOptionalNonNegativeNumber(msg.thinkingTailMs, "thinkingTailMs");
1771
2216
  } catch (err) {
1772
2217
  return {
1773
- unicast: formatSendAck(
2218
+ unicast: formatSendAckCompat(
1774
2219
  id,
1775
2220
  "rejected",
1776
2221
  err && err.message ? err.message : "Invalid simulateStream parameters",
@@ -1793,10 +2238,10 @@ function createDownstreamHandler(opts) {
1793
2238
  (result) => {
1794
2239
  const status = result && result.status ? result.status : "accepted";
1795
2240
  const error = result && result.error ? result.error : undefined;
1796
- return { unicast: formatSendAck(id, status, error) };
2241
+ return { unicast: formatSendAckCompat(id, status, error) };
1797
2242
  },
1798
2243
  (err) => ({
1799
- unicast: formatSendAck(
2244
+ unicast: formatSendAckCompat(
1800
2245
  id,
1801
2246
  "rejected",
1802
2247
  err && err.message ? err.message : "simulateStream failed",
@@ -2059,6 +2504,41 @@ function createDownstreamHandler(opts) {
2059
2504
  );
2060
2505
  }
2061
2506
 
2507
+ /**
2508
+ * Handle "getProviderUsageSnapshot": return the current provider usage snapshot.
2509
+ *
2510
+ * @param {string} clientId
2511
+ * @returns {Promise<{ unicast: string }>|{ unicast: string }}
2512
+ */
2513
+ function handleGetProviderUsageSnapshot(clientId) {
2514
+ const emptySnapshot = () => ({
2515
+ sessionKey: null,
2516
+ provider: null,
2517
+ displayName: null,
2518
+ limitingWindowKey: null,
2519
+ windows: [],
2520
+ fetchedAtMs: Date.now(),
2521
+ stale: true,
2522
+ });
2523
+
2524
+ if (!onGetProviderUsageSnapshot) {
2525
+ return {
2526
+ unicast: formatProviderUsageSnapshot(emptySnapshot()),
2527
+ };
2528
+ }
2529
+ return Promise.resolve(onGetProviderUsageSnapshot()).then(
2530
+ (payload) => ({
2531
+ unicast: formatProviderUsageSnapshot(payload || {}),
2532
+ }),
2533
+ (err) => {
2534
+ logger.error(`[downstream] getProviderUsageSnapshot failed: ${err.message}`);
2535
+ return {
2536
+ unicast: formatProviderUsageSnapshot(emptySnapshot()),
2537
+ };
2538
+ },
2539
+ );
2540
+ }
2541
+
2062
2542
  /**
2063
2543
  * Handle "getStatus": return the current status snapshot.
2064
2544
  *
@@ -2143,6 +2623,53 @@ function createDownstreamHandler(opts) {
2143
2623
  );
2144
2624
  }
2145
2625
 
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
+ function handleCompactSession(clientId, msg) {
2634
+ if (!onCompactSession) {
2635
+ return Promise.resolve({
2636
+ unicast: formatCompactSessionAck({
2637
+ status: "rejected",
2638
+ requestId: msg && msg.requestId,
2639
+ error: "compactSession is not available",
2640
+ }),
2641
+ });
2642
+ }
2643
+ const sessionKey =
2644
+ msg && typeof msg.sessionKey === "string" && msg.sessionKey
2645
+ ? msg.sessionKey
2646
+ : null;
2647
+ if (!sessionKey) {
2648
+ return Promise.resolve({
2649
+ unicast: formatCompactSessionAck({
2650
+ status: "rejected",
2651
+ requestId: msg && msg.requestId,
2652
+ error: "sessionKey is required",
2653
+ }),
2654
+ });
2655
+ }
2656
+ return Promise.resolve(onCompactSession({ sessionKey })).then(
2657
+ (result) => ({
2658
+ unicast: formatCompactSessionAck({
2659
+ ...(result || { status: "accepted" }),
2660
+ requestId: msg.requestId,
2661
+ }),
2662
+ }),
2663
+ (err) => ({
2664
+ unicast: formatCompactSessionAck({
2665
+ status: "rejected",
2666
+ requestId: msg.requestId,
2667
+ error: err && err.message ? err.message : "compactSession failed",
2668
+ }),
2669
+ }),
2670
+ );
2671
+ }
2672
+
2146
2673
  /**
2147
2674
  * Handle "getEvenAiSettings": read relay-owned Even AI settings.
2148
2675
  *
@@ -2338,6 +2865,65 @@ function createDownstreamHandler(opts) {
2338
2865
  );
2339
2866
  }
2340
2867
 
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
+ function handleSetUserSessionTitle(clientId, msg) {
2877
+ if (typeof onSetUserSessionTitle !== "function") return null;
2878
+ const sessionKey =
2879
+ typeof msg.sessionKey === "string" ? msg.sessionKey.trim() : "";
2880
+ const title = typeof msg.title === "string" ? msg.title.trim() : "";
2881
+ if (!sessionKey || !title) return null;
2882
+ if (title.length > 55) return null;
2883
+ onSetUserSessionTitle(sessionKey, title);
2884
+ return null;
2885
+ }
2886
+
2887
+ function handleSetSessionPinned(clientId, msg) {
2888
+ if (typeof onSetSessionPinned !== "function") return null;
2889
+ const sessionKey =
2890
+ typeof msg.sessionKey === "string" ? msg.sessionKey.trim() : "";
2891
+ const pinned = msg.pinned === true;
2892
+ const kind = msg.kind;
2893
+ if (!sessionKey || (kind !== "ocuclaw" && kind !== "evenai")) {
2894
+ return { unicast: formatError("invalid_session_pin_request") };
2895
+ }
2896
+ const result = onSetSessionPinned(sessionKey, pinned, kind);
2897
+ if (result && result.ok === false) {
2898
+ const code = result.reason === "cap" ? "pin_cap_reached" : "invalid_session_pin_request";
2899
+ return { unicast: formatError(code) };
2900
+ }
2901
+ return null;
2902
+ }
2903
+
2904
+ function handleDeleteSessions(clientId, msg) {
2905
+ if (typeof onDeleteSessions !== "function") return null;
2906
+ const sessionKeys = Array.isArray(msg.sessionKeys) ? msg.sessionKeys.filter((k) => typeof k === "string" && k) : [];
2907
+ const kind = msg.kind;
2908
+ const switchBeforeDelete = msg.switchBeforeDelete === true;
2909
+ if (sessionKeys.length === 0 || (kind !== "ocuclaw" && kind !== "evenai")) {
2910
+ return { unicast: formatError("invalid_session_delete_request") };
2911
+ }
2912
+ onDeleteSessions(sessionKeys, kind, switchBeforeDelete);
2913
+ return null;
2914
+ }
2915
+
2916
+ function handleSearchTranscripts(clientId, msg) {
2917
+ if (typeof onSearchTranscripts !== "function") return null;
2918
+ const query = typeof msg.query === "string" ? msg.query : "";
2919
+ const kind = msg.kind;
2920
+ if (!query.trim() || (kind !== "ocuclaw" && kind !== "evenai")) {
2921
+ return { unicast: formatError("invalid_transcript_search_request") };
2922
+ }
2923
+ onSearchTranscripts(clientId, query, kind);
2924
+ return null;
2925
+ }
2926
+
2341
2927
  /**
2342
2928
  * Handle a "slashCommand" message: forward a slash command to OpenClaw.
2343
2929
  *
@@ -2541,6 +3127,36 @@ function createDownstreamHandler(opts) {
2541
3127
  }
2542
3128
  }
2543
3129
 
3130
+ function handleTraceLogSet(clientId, msg) {
3131
+ if (!onTraceLogSet) {
3132
+ return { unicast: formatError("trace-log-set is not available") };
3133
+ }
3134
+ let payload;
3135
+ try {
3136
+ payload = parseTraceLogSet(msg);
3137
+ } catch (err) {
3138
+ return { unicast: formatError(err.message) };
3139
+ }
3140
+ try {
3141
+ const result = onTraceLogSet(clientId, payload) || { ok: true };
3142
+ return { unicast: formatTraceLog(result) };
3143
+ } catch (err) {
3144
+ return { unicast: formatError(err.message || "trace-log-set failed") };
3145
+ }
3146
+ }
3147
+
3148
+ function handleTraceLogGet(clientId) {
3149
+ if (!onTraceLogGet) {
3150
+ return { unicast: formatError("trace-log-get is not available") };
3151
+ }
3152
+ try {
3153
+ const result = onTraceLogGet(clientId) || { ok: true };
3154
+ return { unicast: formatTraceLog(result) };
3155
+ } catch (err) {
3156
+ return { unicast: formatError(err.message || "trace-log-get failed") };
3157
+ }
3158
+ }
3159
+
2544
3160
  /**
2545
3161
  * Handle a "remote-control" message: dispatch remote actions to app clients.
2546
3162
  */
@@ -2591,6 +3207,139 @@ function createDownstreamHandler(opts) {
2591
3207
  }
2592
3208
  }
2593
3209
 
3210
+ function handleReadinessProbe(clientId, msg) {
3211
+ if (!onReadinessProbe) {
3212
+ return { unicast: formatError("readiness probe is not available") };
3213
+ }
3214
+
3215
+ let payload;
3216
+ try {
3217
+ payload = parseReadinessProbe(msg);
3218
+ } catch (err) {
3219
+ return { unicast: formatError(err.message) };
3220
+ }
3221
+
3222
+ const finalize = (result) => {
3223
+ const resolved = result || {};
3224
+ const requestId = resolved.requestId || payload.requestId;
3225
+ if (
3226
+ resolved.ok === false ||
3227
+ !resolved.targetClientId ||
3228
+ !resolved.probe
3229
+ ) {
3230
+ return {
3231
+ unicast: formatReadinessProbeAck({
3232
+ ok: false,
3233
+ requestId,
3234
+ reasonCode: resolved.reasonCode || null,
3235
+ message: resolved.message || "readiness probe was not dispatched",
3236
+ activeSessionKey: resolved.activeSessionKey || null,
3237
+ emittedAtMs: resolved.emittedAtMs || null,
3238
+ }),
3239
+ };
3240
+ }
3241
+
3242
+ return {
3243
+ readinessProbe: {
3244
+ requestId,
3245
+ targetClientId: resolved.targetClientId,
3246
+ message: formatReadinessProbeRequest(resolved.probe),
3247
+ },
3248
+ };
3249
+ };
3250
+
3251
+ try {
3252
+ const result = onReadinessProbe(clientId, payload);
3253
+ if (result && typeof result.then === "function") {
3254
+ return result.then(
3255
+ (resolved) => finalize(resolved),
3256
+ (err) => ({
3257
+ unicast: formatError(err.message || "readiness probe failed"),
3258
+ }),
3259
+ );
3260
+ }
3261
+ return finalize(result);
3262
+ } catch (err) {
3263
+ return { unicast: formatError(err.message || "readiness probe failed") };
3264
+ }
3265
+ }
3266
+
3267
+ function handleAutomationState(clientId, msg) {
3268
+ let payload;
3269
+ try {
3270
+ payload = parseAutomationStateGet(msg);
3271
+ } catch (err) {
3272
+ return {
3273
+ unicast: formatAutomationStateSnapshot({
3274
+ ok: false,
3275
+ requestId:
3276
+ msg && typeof msg.requestId === "string" ? msg.requestId : null,
3277
+ reasonCode: "snapshot_unavailable",
3278
+ message: err.message,
3279
+ }),
3280
+ };
3281
+ }
3282
+
3283
+ if (!onAutomationState) {
3284
+ return null;
3285
+ }
3286
+
3287
+ const finalize = (result) => {
3288
+ const resolved = result || {};
3289
+ const requestId = resolved.requestId || payload.requestId;
3290
+ if (
3291
+ resolved.ok === false ||
3292
+ !resolved.targetClientId ||
3293
+ !resolved.request
3294
+ ) {
3295
+ return {
3296
+ unicast: formatAutomationStateSnapshot({
3297
+ ok: false,
3298
+ requestId,
3299
+ reasonCode: resolved.reasonCode || "snapshot_unavailable",
3300
+ message:
3301
+ resolved.message || "automation state request was not dispatched",
3302
+ }),
3303
+ };
3304
+ }
3305
+
3306
+ return {
3307
+ automationStateRequest: {
3308
+ requestId,
3309
+ targetClientId: resolved.targetClientId,
3310
+ message: formatAutomationStateRequest(resolved.request),
3311
+ },
3312
+ };
3313
+ };
3314
+
3315
+ try {
3316
+ const result = onAutomationState(clientId, payload);
3317
+ if (result && typeof result.then === "function") {
3318
+ return result.then(
3319
+ (resolved) => finalize(resolved),
3320
+ (err) => ({
3321
+ unicast: formatAutomationStateSnapshot({
3322
+ ok: false,
3323
+ requestId: payload.requestId,
3324
+ reasonCode: "snapshot_unavailable",
3325
+ message: err.message || "automation state request failed",
3326
+ }),
3327
+ }),
3328
+ );
3329
+ }
3330
+ return finalize(result);
3331
+ } catch (err) {
3332
+ return {
3333
+ unicast: formatAutomationStateSnapshot({
3334
+ ok: false,
3335
+ requestId: payload.requestId,
3336
+ reasonCode: "snapshot_unavailable",
3337
+ message: err.message || "automation state request failed",
3338
+ }),
3339
+ };
3340
+ }
3341
+ }
3342
+
2594
3343
  // --- Public API ---
2595
3344
 
2596
3345
  return {
@@ -2639,6 +3388,14 @@ function createDownstreamHandler(opts) {
2639
3388
  return handleSwitchSession(clientId, msg);
2640
3389
  case APP_PROTOCOL.sessionCreate:
2641
3390
  return handleNewSession(clientId);
3391
+ case APP_PROTOCOL.sessionTitleSet:
3392
+ return handleSetUserSessionTitle(clientId, msg);
3393
+ case "ocuclaw.session.pinned.set":
3394
+ return handleSetSessionPinned(clientId, msg);
3395
+ case "ocuclaw.session.delete":
3396
+ return handleDeleteSessions(clientId, msg);
3397
+ case "ocuclaw.session.transcripts.search":
3398
+ return handleSearchTranscripts(clientId, msg);
2642
3399
  case APP_PROTOCOL.modelCatalogGet:
2643
3400
  return handleGetModelsCatalog(clientId);
2644
3401
  case APP_PROTOCOL.skillsCatalogGet:
@@ -2647,6 +3404,9 @@ function createDownstreamHandler(opts) {
2647
3404
  case APP_PROTOCOL.sonioxModelsGet:
2648
3405
  case "getSonioxModels":
2649
3406
  return handleGetSonioxModels(clientId);
3407
+ case APP_PROTOCOL.providerUsageGet:
3408
+ case "getProviderUsageSnapshot":
3409
+ return handleGetProviderUsageSnapshot(clientId);
2650
3410
  case APP_PROTOCOL.statusGet:
2651
3411
  case "getStatus":
2652
3412
  return handleGetStatus(clientId);
@@ -2654,6 +3414,8 @@ function createDownstreamHandler(opts) {
2654
3414
  return handleGetSessionModelConfig(clientId);
2655
3415
  case APP_PROTOCOL.sessionConfigSet:
2656
3416
  return handleSetSessionModelConfig(clientId, msg);
3417
+ case APP_PROTOCOL.sessionCompact:
3418
+ return handleCompactSession(clientId, msg);
2657
3419
  case APP_PROTOCOL.evenAiSettingsGet:
2658
3420
  return handleGetEvenAiSettings(clientId);
2659
3421
  case APP_PROTOCOL.evenAiSessionList:
@@ -2679,10 +3441,83 @@ function createDownstreamHandler(opts) {
2679
3441
  return handleDebugSet(clientId, msg);
2680
3442
  case "debug-dump":
2681
3443
  return handleDebugDump(clientId, msg);
3444
+ case "trace-log-set":
3445
+ return handleTraceLogSet(clientId, msg);
3446
+ case "trace-log-get":
3447
+ return handleTraceLogGet(clientId);
2682
3448
  case "remote-control":
2683
3449
  return handleRemoteControl(clientId, msg);
3450
+ case APP_PROTOCOL.automationStateGet:
3451
+ return handleAutomationState(clientId, msg);
3452
+ case APP_PROTOCOL.readinessProbeRequest:
3453
+ return handleReadinessProbe(clientId, msg);
2684
3454
  case APP_PROTOCOL.debugEvent:
2685
3455
  return handleEventDebug(clientId, msg);
3456
+ case "glasses_ui_result":
3457
+ if (typeof onGlassesUiResult === "function") {
3458
+ try {
3459
+ onGlassesUiResult({
3460
+ surfaceId: typeof msg.surfaceId === "string" ? msg.surfaceId : "",
3461
+ outcome: msg.outcome,
3462
+ });
3463
+ } catch (err) {
3464
+ logger.warn(
3465
+ `[downstream] glasses_ui_result handler threw: ${err && err.message ? err.message : err}`,
3466
+ );
3467
+ }
3468
+ }
3469
+ return null;
3470
+ 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.
3475
+ if (typeof onGlassesUiNavEvent === "function") {
3476
+ try {
3477
+ onGlassesUiNavEvent({
3478
+ surfaceId: typeof msg.surfaceId === "string" ? msg.surfaceId : "",
3479
+ depth: Number.isFinite(msg.depth) ? Math.max(1, Math.floor(msg.depth)) : 1,
3480
+ });
3481
+ } catch (err) {
3482
+ logger.warn(
3483
+ `[downstream] glasses_ui_nav_event handler threw: ${err && err.message ? err.message : err}`,
3484
+ );
3485
+ }
3486
+ }
3487
+ return null;
3488
+ 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.
3491
+ if (typeof onGlassesUiRenderInject === "function") {
3492
+ try {
3493
+ onGlassesUiRenderInject({
3494
+ surfaceId: typeof msg.surfaceId === "string" ? msg.surfaceId : "",
3495
+ depth: Number.isFinite(msg.depth) ? Math.max(1, Math.floor(msg.depth)) : 1,
3496
+ spec: msg.spec,
3497
+ });
3498
+ } catch (err) {
3499
+ logger.warn(
3500
+ `[downstream] glasses_ui_render inject handler threw: ${err && err.message ? err.message : err}`,
3501
+ );
3502
+ }
3503
+ }
3504
+ return null;
3505
+ case "device_info_response":
3506
+ if (typeof onDeviceInfoResponse === "function") {
3507
+ try {
3508
+ onDeviceInfoResponse({
3509
+ requestId: typeof msg.requestId === "string" ? msg.requestId : "",
3510
+ ok: msg.ok === true,
3511
+ code: typeof msg.code === "string" ? msg.code : undefined,
3512
+ data: msg.data && typeof msg.data === "object" ? msg.data : undefined,
3513
+ });
3514
+ } catch (err) {
3515
+ logger.warn(
3516
+ `[downstream] device_info_response handler threw: ${err && err.message ? err.message : err}`,
3517
+ );
3518
+ }
3519
+ }
3520
+ return null;
2686
3521
  default:
2687
3522
  return null;
2688
3523
  }
@@ -2691,7 +3526,8 @@ function createDownstreamHandler(opts) {
2691
3526
  formatPages,
2692
3527
  formatStatus,
2693
3528
  formatActivity,
2694
- formatSendAck,
3529
+ formatTyping,
3530
+ formatSendAck: formatSendAckCompat,
2695
3531
  formatProtocol,
2696
3532
  formatStreaming,
2697
3533
  formatSessions,
@@ -2699,6 +3535,7 @@ function createDownstreamHandler(opts) {
2699
3535
  formatModelsCatalog,
2700
3536
  formatSkillsCatalog,
2701
3537
  formatSonioxModels,
3538
+ formatProviderUsageSnapshot,
2702
3539
  formatSessionModelConfig,
2703
3540
  formatSessionModelConfigAck,
2704
3541
  formatEvenAiSettings,
@@ -2709,8 +3546,8 @@ function createDownstreamHandler(opts) {
2709
3546
  formatApproval,
2710
3547
  formatApprovalResolved,
2711
3548
  formatApprovalResponseAck,
2712
- formatTranscription,
2713
3549
  formatListenCommitted,
3550
+ formatEvenAiListenIntercepted,
2714
3551
  formatListenEnded,
2715
3552
  formatListenError,
2716
3553
  formatListenReady,
@@ -2721,6 +3558,10 @@ function createDownstreamHandler(opts) {
2721
3558
  formatDebugConfigSnapshot,
2722
3559
  formatRemoteControl,
2723
3560
  formatRemoteControlAck,
3561
+ formatAutomationStateRequest,
3562
+ formatAutomationStateSnapshot,
3563
+ formatReadinessProbeRequest,
3564
+ formatReadinessProbeAck,
2724
3565
  formatError,
2725
3566
 
2726
3567
  /**