ocuclaw 1.2.4 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +21 -6
  2. package/dist/config/runtime-config.js +84 -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 +56 -182
  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 +754 -83
  27. package/dist/runtime/ocuclaw-settings-store.js +74 -31
  28. package/dist/runtime/plugin-version-service.js +23 -0
  29. package/dist/runtime/protocol-adapter.js +9 -0
  30. package/dist/runtime/provider-usage-select.js +168 -0
  31. package/dist/runtime/relay-client-nudge-controller.js +553 -0
  32. package/dist/runtime/relay-core.js +1293 -225
  33. package/dist/runtime/relay-health-monitor.js +172 -0
  34. package/dist/runtime/relay-operation-registry.js +263 -0
  35. package/dist/runtime/relay-service.js +201 -1
  36. package/dist/runtime/relay-worker-approval-replay-cache.js +68 -0
  37. package/dist/runtime/relay-worker-entry.js +32 -0
  38. package/dist/runtime/relay-worker-health.js +272 -0
  39. package/dist/runtime/relay-worker-protocol.js +281 -0
  40. package/dist/runtime/relay-worker-queue.js +202 -0
  41. package/dist/runtime/relay-worker-supervisor.js +1004 -0
  42. package/dist/runtime/relay-worker-transport.js +1051 -0
  43. package/dist/runtime/session-context-service.js +189 -0
  44. package/dist/runtime/session-service.js +638 -27
  45. package/dist/runtime/upstream-runtime.js +1167 -60
  46. package/dist/tools/device-info-tool.js +242 -0
  47. package/dist/tools/glasses-ui-cron.js +427 -0
  48. package/dist/tools/glasses-ui-descriptors.js +261 -0
  49. package/dist/tools/glasses-ui-limits.js +21 -0
  50. package/dist/tools/glasses-ui-paint-floor.js +99 -0
  51. package/dist/tools/glasses-ui-recipes.js +581 -0
  52. package/dist/tools/glasses-ui-surfaces.js +278 -0
  53. package/dist/tools/glasses-ui-template.js +182 -0
  54. package/dist/tools/glasses-ui-tool.js +1111 -0
  55. package/dist/tools/session-title-tool.js +209 -0
  56. package/dist/version.js +2 -0
  57. package/openclaw.plugin.json +163 -15
  58. package/package.json +14 -5
  59. package/skills/glasses-ui/SKILL.md +156 -0
  60. package/dist/runtime/downstream-server.js +0 -1891
@@ -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]
@@ -81,8 +89,10 @@ function createDownstreamHandler(opts) {
81
89
  const onGetModelsCatalog = opts.onGetModelsCatalog;
82
90
  const onGetSkillsCatalog = opts.onGetSkillsCatalog;
83
91
  const onGetSonioxModels = opts.onGetSonioxModels || null;
92
+ const onGetProviderUsageSnapshot = opts.onGetProviderUsageSnapshot || null;
84
93
  const onGetSessionModelConfig = opts.onGetSessionModelConfig;
85
94
  const onSetSessionModelConfig = opts.onSetSessionModelConfig;
95
+ const onCompactSession = opts.onCompactSession || null;
86
96
  const onGetEvenAiSettings = opts.onGetEvenAiSettings;
87
97
  const onGetEvenAiSessions = opts.onGetEvenAiSessions;
88
98
  const onSetEvenAiSettings = opts.onSetEvenAiSettings;
@@ -96,9 +106,21 @@ function createDownstreamHandler(opts) {
96
106
  const onDebugSet = opts.onDebugSet || null;
97
107
  const onDebugDump = opts.onDebugDump || null;
98
108
  const onEventDebug = opts.onEventDebug || null;
109
+ const onTraceLogSet = opts.onTraceLogSet || null;
110
+ const onTraceLogGet = opts.onTraceLogGet || null;
99
111
  const onRemoteControl = opts.onRemoteControl || null;
112
+ const onAutomationState = opts.onAutomationState || null;
100
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;
101
122
  const getSnapshotRevision = opts.getSnapshotRevision || null;
123
+ const operationRegistry = opts.operationRegistry || null;
102
124
 
103
125
  /** Client IDs subscribed to raw protocol frame forwarding. */
104
126
  const protocolSubscribers = new Set();
@@ -115,6 +137,8 @@ function createDownstreamHandler(opts) {
115
137
  const approvalResolveCache = new Map();
116
138
  const APP_PROTOCOL = {
117
139
  activity: "ocuclaw.activity.update",
140
+ automationStateGet: "ocuclaw.automation.state.get",
141
+ automationStateSnapshot: "ocuclaw.automation.state.snapshot",
118
142
  approvalRequest: "ocuclaw.approval.request",
119
143
  approvalResolve: "ocuclaw.approval.resolve",
120
144
  approvalResolveAck: "ocuclaw.approval.resolve.ack",
@@ -137,6 +161,8 @@ function createDownstreamHandler(opts) {
137
161
  messageStreamDelta: "ocuclaw.message.stream.delta",
138
162
  modelCatalogGet: "ocuclaw.model.catalog.get",
139
163
  modelCatalogSnapshot: "ocuclaw.model.catalog.snapshot",
164
+ providerUsageGet: "ocuclaw.provider.usage.get",
165
+ providerUsageSnapshot: "ocuclaw.provider.usage.snapshot",
140
166
  skillsCatalogGet: "ocuclaw.skills.catalog.get",
141
167
  skillsCatalogSnapshot: "ocuclaw.skills.catalog.snapshot",
142
168
  pages: "ocuclaw.view.pages.snapshot",
@@ -152,16 +178,20 @@ function createDownstreamHandler(opts) {
152
178
  sessionConfigSet: "ocuclaw.session.config.set",
153
179
  sessionConfigSetAck: "ocuclaw.session.config.set.ack",
154
180
  sessionConfigSnapshot: "ocuclaw.session.config.snapshot",
181
+ sessionCompact: "ocuclaw.session.compact",
182
+ sessionCompactAck: "ocuclaw.session.compact.ack",
155
183
  sessionCreate: "ocuclaw.session.create",
156
184
  sessionList: "ocuclaw.session.list",
157
185
  sessionListResult: "ocuclaw.session.list.result",
158
186
  sessionReset: "ocuclaw.session.reset",
159
187
  sessionSwitch: "ocuclaw.session.switch",
160
188
  sessionSwitchApplied: "ocuclaw.session.switch.applied",
189
+ sessionTitleSet: "ocuclaw.session.title.set",
161
190
  sonioxTemporaryKey: "sonioxTemporaryKey",
162
191
  sonioxTemporaryKeyError: "sonioxTemporaryKeyError",
163
192
  status: "ocuclaw.runtime.status",
164
193
  statusGet: "ocuclaw.runtime.status.get",
194
+ typingUpdate: "ocuclaw.typing.update",
165
195
  };
166
196
 
167
197
  // --- Format helpers ---
@@ -222,25 +252,52 @@ function createDownstreamHandler(opts) {
222
252
  return JSON.stringify({ ...activity, type: APP_PROTOCOL.activity });
223
253
  }
224
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
+
225
265
  /**
226
266
  * Format an error message for unicast.
227
267
  *
228
268
  * @param {string} error
269
+ * @param {{code?: string, requestId?: string, op?: string}} [meta]
229
270
  * @returns {string} JSON string
230
271
  */
231
- function formatError(error) {
232
- return JSON.stringify({
272
+ function formatError(error, meta) {
273
+ const msg = {
233
274
  type: "error",
234
275
  error: error || "Unknown error",
235
- });
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);
236
289
  }
237
290
 
238
291
  function isExternalDebugToolMessageType(messageType) {
239
292
  return (
240
293
  messageType === "debug-set" ||
241
294
  messageType === "debug-dump" ||
295
+ messageType === "trace-log-set" ||
296
+ messageType === "trace-log-get" ||
242
297
  messageType === "remote-control" ||
243
- messageType === APP_PROTOCOL.readinessProbeRequest
298
+ messageType === APP_PROTOCOL.automationStateGet ||
299
+ messageType === APP_PROTOCOL.readinessProbeRequest ||
300
+ messageType === "glasses_ui_render"
244
301
  );
245
302
  }
246
303
 
@@ -253,15 +310,12 @@ function createDownstreamHandler(opts) {
253
310
  * @param {string} [errorCode] - Structured error code (for deterministic client handling)
254
311
  * @returns {string} JSON string
255
312
  */
256
- function formatSendAck(id, status, error, errorCode) {
257
- const msg = { type: APP_PROTOCOL.messageSendAck, requestId: id, status };
258
- if (error !== undefined) {
259
- msg.error = error;
260
- }
261
- if (errorCode !== undefined) {
262
- msg.errorCode = errorCode;
263
- }
264
- 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);
265
319
  }
266
320
 
267
321
  /**
@@ -283,10 +337,18 @@ function createDownstreamHandler(opts) {
283
337
  * Format a streaming text message for broadcast during agent runs.
284
338
  *
285
339
  * @param {string} text - Streaming assistant text
340
+ * @param {Array<{start: number, end: number, emoji: string}>} [spans] - Optional emoji spans
286
341
  * @returns {string} JSON string
287
342
  */
288
- function formatStreaming(text) {
289
- 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);
290
352
  }
291
353
 
292
354
  /**
@@ -366,6 +428,75 @@ function createDownstreamHandler(opts) {
366
428
  });
367
429
  }
368
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
+
369
500
  /**
370
501
  * Format current session model controls.
371
502
  *
@@ -394,6 +525,11 @@ function createDownstreamHandler(opts) {
394
525
  payload && typeof payload.verboseLevel === "string"
395
526
  ? payload.verboseLevel
396
527
  : "off",
528
+ fastMode: !!(payload && payload.fastMode === true),
529
+ elevatedLevel:
530
+ payload && typeof payload.elevatedLevel === "string"
531
+ ? payload.elevatedLevel
532
+ : "off",
397
533
  });
398
534
  }
399
535
 
@@ -417,6 +553,30 @@ function createDownstreamHandler(opts) {
417
553
  return JSON.stringify(out);
418
554
  }
419
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
+
420
580
  /**
421
581
  * Format the current relay-owned Even AI settings snapshot.
422
582
  *
@@ -444,6 +604,7 @@ function createDownstreamHandler(opts) {
444
604
  ? payload.defaultThinking
445
605
  : "",
446
606
  listenEnabled: payload && payload.listenEnabled === true,
607
+ defaultFastMode: !!(payload && payload.defaultFastMode === true),
447
608
  });
448
609
  }
449
610
 
@@ -491,6 +652,7 @@ function createDownstreamHandler(opts) {
491
652
  ? payload.defaultThinking
492
653
  : undefined,
493
654
  ),
655
+ defaultFastMode: !!(payload && payload.defaultFastMode === true),
494
656
  });
495
657
  }
496
658
 
@@ -539,23 +701,52 @@ function createDownstreamHandler(opts) {
539
701
  */
540
702
  function formatApproval(data) {
541
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;
542
722
  return JSON.stringify({
543
723
  type: APP_PROTOCOL.approvalRequest,
544
724
  id: data.id,
725
+ ...(isPluginApproval ? { approvalKind: "plugin" } : {}),
545
726
  requestId:
546
727
  (data && typeof data.requestId === "string" && data.requestId) ||
547
728
  (request && typeof request.requestId === "string" && request.requestId) ||
548
729
  null,
549
- command: request.command || "",
550
- cwd: request.cwd || null,
730
+ command: commandText,
731
+ cwd: request.cwd || pluginDescription || null,
551
732
  agentId: request.agentId || null,
552
- host: request.host || null,
553
- security: request.security || null,
733
+ host: request.host || (isPluginApproval ? "plugin" : null),
734
+ security: request.security || (isPluginApproval && typeof request.severity === "string" ? request.severity : null),
554
735
  ask: request.ask || null,
555
736
  resolvedPath: request.resolvedPath || null,
556
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
+ : {}),
557
745
  createdAtMs: data.createdAtMs || 0,
558
746
  expiresAtMs: data.expiresAtMs || 0,
747
+ allowedDecisions: Array.isArray(request.allowedDecisions)
748
+ ? request.allowedDecisions.filter((d) => typeof d === "string")
749
+ : null,
559
750
  });
560
751
  }
561
752
 
@@ -611,17 +802,6 @@ function createDownstreamHandler(opts) {
611
802
  });
612
803
  }
613
804
 
614
- /**
615
- * Format a transcription update for broadcast during voice mode.
616
- *
617
- * @param {string} text - Current transcription text
618
- * @param {boolean} isFinal - Whether this is a final transcription
619
- * @returns {string} JSON string
620
- */
621
- function formatTranscription(text, isFinal) {
622
- return JSON.stringify({ type: "transcription", text, final: isFinal });
623
- }
624
-
625
805
  /**
626
806
  * Format a committed listen handoff update for broadcast during voice mode.
627
807
  *
@@ -639,6 +819,13 @@ function createDownstreamHandler(opts) {
639
819
  });
640
820
  }
641
821
 
822
+ function formatEvenAiListenIntercepted(sessionKey) {
823
+ return JSON.stringify({
824
+ type: "even-ai-listen-intercepted",
825
+ sessionKey: sessionKey ?? null,
826
+ });
827
+ }
828
+
642
829
  /**
643
830
  * Format a listen-ended message for broadcast.
644
831
  * @returns {string} JSON string
@@ -728,6 +915,10 @@ function createDownstreamHandler(opts) {
728
915
  : "";
729
916
  const lowered = message.toLowerCase();
730
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
+ }
731
922
  if (lowered.includes("is not available")) {
732
923
  return "soniox_temp_key_unavailable";
733
924
  }
@@ -776,6 +967,27 @@ function createDownstreamHandler(opts) {
776
967
  });
777
968
  }
778
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
+
779
991
  /**
780
992
  * Format the current relay debug-config snapshot for app/WebUI clients.
781
993
  *
@@ -833,6 +1045,41 @@ function createDownstreamHandler(opts) {
833
1045
  });
834
1046
  }
835
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
+
836
1083
  function formatReadinessProbeRequest(data) {
837
1084
  return JSON.stringify({
838
1085
  type: APP_PROTOCOL.readinessProbeRequest,
@@ -963,25 +1210,12 @@ function createDownstreamHandler(opts) {
963
1210
  ) {
964
1211
  throw new Error("debug-dump untilMs must be a non-negative number");
965
1212
  }
966
- if (msg.redaction !== undefined) {
967
- if (typeof msg.redaction !== "string") {
968
- throw new Error("debug-dump redaction must be one of: safe, full");
969
- }
970
- const normalized = msg.redaction.trim().toLowerCase();
971
- if (normalized !== "safe" && normalized !== "full") {
972
- throw new Error("debug-dump redaction must be one of: safe, full");
973
- }
974
- }
975
1213
  return {
976
1214
  categories,
977
1215
  limit: msg.limit === undefined ? undefined : Number(msg.limit),
978
1216
  sinceMs: msg.sinceMs === undefined ? undefined : Number(msg.sinceMs),
979
1217
  sinceAgeMs: msg.sinceAgeMs === undefined ? undefined : Number(msg.sinceAgeMs),
980
1218
  untilMs: msg.untilMs === undefined ? undefined : Number(msg.untilMs),
981
- redaction:
982
- msg.redaction === undefined
983
- ? undefined
984
- : msg.redaction.trim().toLowerCase(),
985
1219
  };
986
1220
  }
987
1221
 
@@ -1187,26 +1421,6 @@ function createDownstreamHandler(opts) {
1187
1421
  ) {
1188
1422
  return "debug-close-app-client";
1189
1423
  }
1190
- if (
1191
- normalized === "debug-screen-off-worker-on" ||
1192
- normalized === "debug_screen_off_worker_on" ||
1193
- normalized === "debugscreenoffworkeron" ||
1194
- normalized === "screen-off-worker-on" ||
1195
- normalized === "screen_off_worker_on" ||
1196
- normalized === "screenoffworkeron"
1197
- ) {
1198
- return "debug-screen-off-worker-on";
1199
- }
1200
- if (
1201
- normalized === "debug-screen-off-worker-off" ||
1202
- normalized === "debug_screen_off_worker_off" ||
1203
- normalized === "debugscreenoffworkeroff" ||
1204
- normalized === "screen-off-worker-off" ||
1205
- normalized === "screen_off_worker_off" ||
1206
- normalized === "screenoffworkeroff"
1207
- ) {
1208
- return "debug-screen-off-worker-off";
1209
- }
1210
1424
  throw new Error(`unsupported remote relayAction: ${raw}`);
1211
1425
  }
1212
1426
 
@@ -1320,6 +1534,20 @@ function createDownstreamHandler(opts) {
1320
1534
  payload.verboseLevel = normalized;
1321
1535
  }
1322
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
+
1323
1551
  if (Object.keys(payload).length === 0) {
1324
1552
  throw new Error("setSessionModelConfig requires at least one field");
1325
1553
  }
@@ -1391,6 +1619,13 @@ function createDownstreamHandler(opts) {
1391
1619
  payload.listenEnabled = msg.listenEnabled;
1392
1620
  }
1393
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
+
1394
1629
  if (Object.keys(payload).length === 0) {
1395
1630
  throw new Error("setEvenAiSettings requires at least one field");
1396
1631
  }
@@ -1426,6 +1661,13 @@ function createDownstreamHandler(opts) {
1426
1661
  payload.defaultThinking = normalizeOcuClawDefaultThinking(msg.defaultThinking);
1427
1662
  }
1428
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
+
1429
1671
  if (Object.keys(payload).length === 0) {
1430
1672
  throw new Error("setOcuClawSettings requires at least one field");
1431
1673
  }
@@ -1518,6 +1760,20 @@ function createDownstreamHandler(opts) {
1518
1760
  return payload;
1519
1761
  }
1520
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
+
1521
1777
  if (action === "relay-action") {
1522
1778
  payload.relayAction = normalizeRemoteRelayAction(msg.relayAction);
1523
1779
  if (
@@ -1598,6 +1854,20 @@ function createDownstreamHandler(opts) {
1598
1854
  };
1599
1855
  }
1600
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
+
1601
1871
  const ATTACHMENT_MAX_DECODED_BYTES = 5_000_000;
1602
1872
  const ATTACHMENT_MAX_ENCODED_CHARS =
1603
1873
  Math.ceil((ATTACHMENT_MAX_DECODED_BYTES * 4) / 3) + 16;
@@ -1736,6 +2006,32 @@ function createDownstreamHandler(opts) {
1736
2006
  };
1737
2007
  }
1738
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
+
1739
2035
  // --- Message handlers ---
1740
2036
 
1741
2037
  /**
@@ -1749,7 +2045,7 @@ function createDownstreamHandler(opts) {
1749
2045
  const requestId = parseOptionalTrimmedString(msg.requestId);
1750
2046
  if (!requestId) {
1751
2047
  return {
1752
- unicast: formatSendAck(
2048
+ unicast: formatSendAckCompat(
1753
2049
  requestId,
1754
2050
  "rejected",
1755
2051
  "Missing required field: requestId",
@@ -1760,7 +2056,7 @@ function createDownstreamHandler(opts) {
1760
2056
  const parsedAttachment = parseAttachment(msg.attachment);
1761
2057
  if (!parsedAttachment.ok) {
1762
2058
  return {
1763
- unicast: formatSendAck(
2059
+ unicast: formatSendAckCompat(
1764
2060
  requestId,
1765
2061
  "rejected",
1766
2062
  parsedAttachment.error,
@@ -1772,7 +2068,7 @@ function createDownstreamHandler(opts) {
1772
2068
  const text = typeof msg.text === "string" ? msg.text : "";
1773
2069
  if (!text.trim() && !parsedAttachment.attachment) {
1774
2070
  return {
1775
- unicast: formatSendAck(
2071
+ unicast: formatSendAckCompat(
1776
2072
  requestId,
1777
2073
  "rejected",
1778
2074
  "Missing required field: text",
@@ -1783,7 +2079,7 @@ function createDownstreamHandler(opts) {
1783
2079
  // Check upstream connectivity
1784
2080
  if (!isUpstreamConnected()) {
1785
2081
  return {
1786
- unicast: formatSendAck(
2082
+ unicast: formatSendAckCompat(
1787
2083
  requestId,
1788
2084
  "rejected",
1789
2085
  "OpenClaw disconnected",
@@ -1791,26 +2087,66 @@ function createDownstreamHandler(opts) {
1791
2087
  };
1792
2088
  }
1793
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
+
1794
2108
  // Forward to upstream — resolves on initial ack (accepted/queued)
1795
- return onSend(
2109
+ const followup = onSend(
1796
2110
  requestId,
1797
2111
  text,
1798
2112
  msg.sessionKey || null,
1799
2113
  parsedAttachment.attachment,
2114
+ clientDisplaySignals,
1800
2115
  ).then(
1801
2116
  (result) => {
1802
2117
  const status = (result && result.status) || "accepted";
1803
- 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 };
1804
2123
  },
1805
- (err) => ({
1806
- unicast: formatSendAck(
2124
+ (err) => {
2125
+ const frame = formatSendAckCompat(
1807
2126
  requestId,
1808
2127
  "rejected",
1809
2128
  err.message || "Send failed",
1810
- err.errorCode || undefined,
1811
- ),
1812
- }),
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
+ },
1813
2139
  );
2140
+
2141
+ return operation
2142
+ ? {
2143
+ unicast: operation.receipt || formatOperationReceived({
2144
+ requestId,
2145
+ operation: "message.send",
2146
+ }),
2147
+ followup,
2148
+ }
2149
+ : followup;
1814
2150
  }
1815
2151
 
1816
2152
  /**
@@ -1839,7 +2175,7 @@ function createDownstreamHandler(opts) {
1839
2175
  const id = parseOptionalTrimmedString(msg.id);
1840
2176
  if (!id) {
1841
2177
  return {
1842
- unicast: formatSendAck(
2178
+ unicast: formatSendAckCompat(
1843
2179
  msg.id || null,
1844
2180
  "rejected",
1845
2181
  "Missing required field: id",
@@ -1849,7 +2185,7 @@ function createDownstreamHandler(opts) {
1849
2185
 
1850
2186
  if (!onSimulateStream) {
1851
2187
  return {
1852
- unicast: formatSendAck(
2188
+ unicast: formatSendAckCompat(
1853
2189
  id,
1854
2190
  "rejected",
1855
2191
  "simulateStream not supported by relay",
@@ -1860,7 +2196,7 @@ function createDownstreamHandler(opts) {
1860
2196
  const text = typeof msg.text === "string" ? msg.text : "";
1861
2197
  if (!text.trim()) {
1862
2198
  return {
1863
- unicast: formatSendAck(
2199
+ unicast: formatSendAckCompat(
1864
2200
  id,
1865
2201
  "rejected",
1866
2202
  "simulateStream requires non-empty text",
@@ -1879,7 +2215,7 @@ function createDownstreamHandler(opts) {
1879
2215
  thinkingTailMs = parseOptionalNonNegativeNumber(msg.thinkingTailMs, "thinkingTailMs");
1880
2216
  } catch (err) {
1881
2217
  return {
1882
- unicast: formatSendAck(
2218
+ unicast: formatSendAckCompat(
1883
2219
  id,
1884
2220
  "rejected",
1885
2221
  err && err.message ? err.message : "Invalid simulateStream parameters",
@@ -1902,10 +2238,10 @@ function createDownstreamHandler(opts) {
1902
2238
  (result) => {
1903
2239
  const status = result && result.status ? result.status : "accepted";
1904
2240
  const error = result && result.error ? result.error : undefined;
1905
- return { unicast: formatSendAck(id, status, error) };
2241
+ return { unicast: formatSendAckCompat(id, status, error) };
1906
2242
  },
1907
2243
  (err) => ({
1908
- unicast: formatSendAck(
2244
+ unicast: formatSendAckCompat(
1909
2245
  id,
1910
2246
  "rejected",
1911
2247
  err && err.message ? err.message : "simulateStream failed",
@@ -2168,6 +2504,41 @@ function createDownstreamHandler(opts) {
2168
2504
  );
2169
2505
  }
2170
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
+
2171
2542
  /**
2172
2543
  * Handle "getStatus": return the current status snapshot.
2173
2544
  *
@@ -2252,6 +2623,53 @@ function createDownstreamHandler(opts) {
2252
2623
  );
2253
2624
  }
2254
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
+
2255
2673
  /**
2256
2674
  * Handle "getEvenAiSettings": read relay-owned Even AI settings.
2257
2675
  *
@@ -2447,6 +2865,65 @@ function createDownstreamHandler(opts) {
2447
2865
  );
2448
2866
  }
2449
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
+
2450
2927
  /**
2451
2928
  * Handle a "slashCommand" message: forward a slash command to OpenClaw.
2452
2929
  *
@@ -2650,6 +3127,36 @@ function createDownstreamHandler(opts) {
2650
3127
  }
2651
3128
  }
2652
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
+
2653
3160
  /**
2654
3161
  * Handle a "remote-control" message: dispatch remote actions to app clients.
2655
3162
  */
@@ -2757,6 +3264,82 @@ function createDownstreamHandler(opts) {
2757
3264
  }
2758
3265
  }
2759
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
+
2760
3343
  // --- Public API ---
2761
3344
 
2762
3345
  return {
@@ -2805,6 +3388,14 @@ function createDownstreamHandler(opts) {
2805
3388
  return handleSwitchSession(clientId, msg);
2806
3389
  case APP_PROTOCOL.sessionCreate:
2807
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);
2808
3399
  case APP_PROTOCOL.modelCatalogGet:
2809
3400
  return handleGetModelsCatalog(clientId);
2810
3401
  case APP_PROTOCOL.skillsCatalogGet:
@@ -2813,6 +3404,9 @@ function createDownstreamHandler(opts) {
2813
3404
  case APP_PROTOCOL.sonioxModelsGet:
2814
3405
  case "getSonioxModels":
2815
3406
  return handleGetSonioxModels(clientId);
3407
+ case APP_PROTOCOL.providerUsageGet:
3408
+ case "getProviderUsageSnapshot":
3409
+ return handleGetProviderUsageSnapshot(clientId);
2816
3410
  case APP_PROTOCOL.statusGet:
2817
3411
  case "getStatus":
2818
3412
  return handleGetStatus(clientId);
@@ -2820,6 +3414,8 @@ function createDownstreamHandler(opts) {
2820
3414
  return handleGetSessionModelConfig(clientId);
2821
3415
  case APP_PROTOCOL.sessionConfigSet:
2822
3416
  return handleSetSessionModelConfig(clientId, msg);
3417
+ case APP_PROTOCOL.sessionCompact:
3418
+ return handleCompactSession(clientId, msg);
2823
3419
  case APP_PROTOCOL.evenAiSettingsGet:
2824
3420
  return handleGetEvenAiSettings(clientId);
2825
3421
  case APP_PROTOCOL.evenAiSessionList:
@@ -2845,12 +3441,83 @@ function createDownstreamHandler(opts) {
2845
3441
  return handleDebugSet(clientId, msg);
2846
3442
  case "debug-dump":
2847
3443
  return handleDebugDump(clientId, msg);
3444
+ case "trace-log-set":
3445
+ return handleTraceLogSet(clientId, msg);
3446
+ case "trace-log-get":
3447
+ return handleTraceLogGet(clientId);
2848
3448
  case "remote-control":
2849
3449
  return handleRemoteControl(clientId, msg);
3450
+ case APP_PROTOCOL.automationStateGet:
3451
+ return handleAutomationState(clientId, msg);
2850
3452
  case APP_PROTOCOL.readinessProbeRequest:
2851
3453
  return handleReadinessProbe(clientId, msg);
2852
3454
  case APP_PROTOCOL.debugEvent:
2853
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;
2854
3521
  default:
2855
3522
  return null;
2856
3523
  }
@@ -2859,7 +3526,8 @@ function createDownstreamHandler(opts) {
2859
3526
  formatPages,
2860
3527
  formatStatus,
2861
3528
  formatActivity,
2862
- formatSendAck,
3529
+ formatTyping,
3530
+ formatSendAck: formatSendAckCompat,
2863
3531
  formatProtocol,
2864
3532
  formatStreaming,
2865
3533
  formatSessions,
@@ -2867,6 +3535,7 @@ function createDownstreamHandler(opts) {
2867
3535
  formatModelsCatalog,
2868
3536
  formatSkillsCatalog,
2869
3537
  formatSonioxModels,
3538
+ formatProviderUsageSnapshot,
2870
3539
  formatSessionModelConfig,
2871
3540
  formatSessionModelConfigAck,
2872
3541
  formatEvenAiSettings,
@@ -2877,8 +3546,8 @@ function createDownstreamHandler(opts) {
2877
3546
  formatApproval,
2878
3547
  formatApprovalResolved,
2879
3548
  formatApprovalResponseAck,
2880
- formatTranscription,
2881
3549
  formatListenCommitted,
3550
+ formatEvenAiListenIntercepted,
2882
3551
  formatListenEnded,
2883
3552
  formatListenError,
2884
3553
  formatListenReady,
@@ -2889,6 +3558,8 @@ function createDownstreamHandler(opts) {
2889
3558
  formatDebugConfigSnapshot,
2890
3559
  formatRemoteControl,
2891
3560
  formatRemoteControlAck,
3561
+ formatAutomationStateRequest,
3562
+ formatAutomationStateSnapshot,
2892
3563
  formatReadinessProbeRequest,
2893
3564
  formatReadinessProbeAck,
2894
3565
  formatError,