ocuclaw 1.3.2 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +29 -1
  2. package/dist/config/runtime-config-session-title-model.test.js +0 -3
  3. package/dist/config/runtime-config.js +22 -33
  4. package/dist/domain/activity-status-adapter.js +0 -7
  5. package/dist/domain/activity-status-arbiter.js +3 -27
  6. package/dist/domain/activity-status-labels.js +8 -38
  7. package/dist/domain/code-span-regions.js +4 -24
  8. package/dist/domain/constant-time-equal.js +9 -0
  9. package/dist/domain/constant-time-equal.test.js +28 -0
  10. package/dist/domain/conversation-state.js +27 -138
  11. package/dist/domain/debug-bundle-cache.js +52 -0
  12. package/dist/domain/debug-bundle-format.js +60 -0
  13. package/dist/domain/debug-bundle-preview.js +123 -0
  14. package/dist/domain/debug-bundle-redaction.js +182 -0
  15. package/dist/domain/debug-bundle-save.js +11 -0
  16. package/dist/domain/debug-bundle-zip.js +15 -0
  17. package/dist/domain/debug-bundle.js +97 -0
  18. package/dist/domain/debug-store.js +6 -17
  19. package/dist/domain/debug-upload-preset.js +27 -0
  20. package/dist/domain/glasses-display-system-prompt.js +0 -5
  21. package/dist/domain/glasses-display-system-prompt.test.js +1 -1
  22. package/dist/domain/glasses-ui-content-summary.js +0 -6
  23. package/dist/domain/glasses-ui-system-prompt.test.js +1 -2
  24. package/dist/domain/message-emoji-allowlist.js +0 -7
  25. package/dist/domain/message-emoji-filter.js +3 -9
  26. package/dist/domain/neural-emoji-reactor-tag-config.js +3 -3
  27. package/dist/domain/prompt-channel-fragments.js +1 -10
  28. package/dist/domain/tagged-span-parser.js +3 -26
  29. package/dist/domain/tagged-span-strip.js +0 -7
  30. package/dist/even-ai/even-ai-endpoint.js +77 -24
  31. package/dist/even-ai/even-ai-run-waiter.js +0 -1
  32. package/dist/even-ai/even-ai-settings-store.js +11 -0
  33. package/dist/gateway/gateway-bridge.js +8 -9
  34. package/dist/gateway/gateway-timing-ledger.js +8 -6
  35. package/dist/gateway/openclaw-client.js +97 -297
  36. package/dist/gateway/sanitize-connect-reason.js +10 -0
  37. package/dist/gateway/sanitize-connect-reason.test.js +34 -0
  38. package/dist/index.js +3 -3
  39. package/dist/runtime/channel-two-hook.js +1 -6
  40. package/dist/runtime/container-env.js +1 -5
  41. package/dist/runtime/debug-bundle-handler.js +159 -0
  42. package/dist/runtime/display-toggle-states.js +6 -17
  43. package/dist/runtime/downstream-handler.js +682 -508
  44. package/dist/runtime/glasses-backpressure-latch.js +93 -0
  45. package/dist/runtime/ocuclaw-settings-store.js +10 -1
  46. package/dist/runtime/openclaw-host-version.js +5 -0
  47. package/dist/runtime/plugin-version-service.js +13 -6
  48. package/dist/runtime/provider-usage-select.js +0 -6
  49. package/dist/runtime/register-session-title-distiller.js +14 -16
  50. package/dist/runtime/relay-core.js +657 -271
  51. package/dist/runtime/relay-service.js +40 -36
  52. package/dist/runtime/relay-worker-approval-replay-cache.js +1 -1
  53. package/dist/runtime/relay-worker-entry.js +1 -2
  54. package/dist/runtime/relay-worker-health.js +2 -10
  55. package/dist/runtime/relay-worker-protocol.js +6 -1
  56. package/dist/runtime/relay-worker-supervisor.js +109 -39
  57. package/dist/runtime/relay-worker-transport.js +157 -15
  58. package/dist/runtime/session-context-service.js +5 -45
  59. package/dist/runtime/session-service.js +157 -175
  60. package/dist/runtime/session-title-distiller-budget.js +1 -5
  61. package/dist/runtime/session-title-distiller-helpers.js +14 -24
  62. package/dist/runtime/session-title-distiller.js +109 -122
  63. package/dist/runtime/session-title-record.js +0 -6
  64. package/dist/runtime/stable-prompt-snapshot.js +3 -14
  65. package/dist/runtime/upstream-runtime.js +600 -103
  66. package/dist/tools/device-info-tool.js +4 -21
  67. package/dist/tools/glasses-ui-cron.js +58 -63
  68. package/dist/tools/glasses-ui-descriptors.js +4 -33
  69. package/dist/tools/glasses-ui-limits.js +0 -13
  70. package/dist/tools/glasses-ui-paint-floor.js +22 -34
  71. package/dist/tools/glasses-ui-recipes.js +92 -101
  72. package/dist/tools/glasses-ui-surfaces.js +295 -100
  73. package/dist/tools/glasses-ui-template.js +7 -22
  74. package/dist/tools/glasses-ui-tool-description.test.js +2 -2
  75. package/dist/tools/glasses-ui-tool.js +475 -331
  76. package/dist/tools/glasses-ui-voicemail.js +242 -0
  77. package/dist/tools/glasses-ui-wake.js +195 -0
  78. package/dist/tools/session-title-tool.js +2 -7
  79. package/dist/tools/session-title-tool.test.js +1 -1
  80. package/dist/version.js +3 -2
  81. package/openclaw.plugin.json +60 -13
  82. package/package.json +3 -2
  83. package/skills/glasses-ui/SKILL.md +19 -3
  84. package/dist/runtime/protocol-adapter.js +0 -387
@@ -1,4 +1,5 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
+ import { constantTimeEqual } from "../domain/constant-time-equal.js";
2
3
  import { filterRawEmojiText } from "../domain/message-emoji-filter.js";
3
4
  import { composeReadabilitySystemPrompt } from "../domain/readability-system-prompt.js";
4
5
  import { normalizeEvenAiSystemPrompt } from "./even-ai-settings-store.js";
@@ -7,6 +8,8 @@ const DEFAULT_RESPONSE_MODEL = "ocuclaw-active-session";
7
8
  const DEFAULT_TIMEOUT_MS = 60000;
8
9
  const DEFAULT_MAX_BODY_BYTES = 65536;
9
10
  const DEFAULT_DEDUP_WINDOW_MS = 500;
11
+
12
+ const DEFAULT_MAX_INTERCEPT_INFLIGHT = 4;
10
13
  export const EVEN_AI_CHAT_COMPLETIONS_PATH = "/v1/chat/completions";
11
14
  const REQUEST_HANDLED_MARKER = Symbol.for("ocuclaw.evenai.handled");
12
15
  function normalizeLogger(logger) {
@@ -277,6 +280,10 @@ export function createEvenAiEndpoint(opts = {}) {
277
280
  typeof opts.seedFastModeForRoute === "function"
278
281
  ? opts.seedFastModeForRoute
279
282
  : null;
283
+ const resolveAgentForRoute =
284
+ typeof opts.resolveAgentForRoute === "function"
285
+ ? opts.resolveAgentForRoute
286
+ : null;
280
287
  const now =
281
288
  typeof opts.now === "function" ? opts.now : () => Date.now();
282
289
  const requestTimeoutMs = normalizePositiveInt(
@@ -291,6 +298,10 @@ export function createEvenAiEndpoint(opts = {}) {
291
298
  0,
292
299
  normalizePositiveInt(opts.dedupWindowMs, DEFAULT_DEDUP_WINDOW_MS),
293
300
  );
301
+ const maxInterceptInflight = normalizePositiveInt(
302
+ opts.maxInterceptInflight,
303
+ DEFAULT_MAX_INTERCEPT_INFLIGHT,
304
+ );
294
305
 
295
306
  if (!gatewayBridge || typeof gatewayBridge.sendMessage !== "function") {
296
307
  throw new Error("Even AI endpoint requires gatewayBridge.sendMessage()");
@@ -310,9 +321,10 @@ export function createEvenAiEndpoint(opts = {}) {
310
321
  throw new Error("Even AI endpoint requires runWaiter.waitForRun()");
311
322
  }
312
323
 
313
- /** @type {{requestId: string, fingerprint: string, sessionKey: string|null, startedAtMs: number}|null} */
314
324
  let inFlight = null;
315
- /** @type {{fingerprint: string, startedAtMs: number}|null} */
325
+
326
+ let interceptInflight = 0;
327
+
316
328
  let lastAccepted = null;
317
329
 
318
330
  async function handleRequest(req, res) {
@@ -346,7 +358,7 @@ export function createEvenAiEndpoint(opts = {}) {
346
358
  }),
347
359
  );
348
360
 
349
- if (!token || authToken !== token) {
361
+ if (!token || !constantTimeEqual(authToken, token)) {
350
362
  emitDebug(
351
363
  "evenai",
352
364
  "request_auth_failed",
@@ -581,6 +593,29 @@ export function createEvenAiEndpoint(opts = {}) {
581
593
  userText,
582
594
  }),
583
595
  ) || "main";
596
+
597
+ if (interceptInflight >= maxInterceptInflight) {
598
+ emitDebug(
599
+ "evenai",
600
+ "listen_intercept_capacity_exceeded",
601
+ "warn",
602
+ { sessionKey },
603
+ () => ({
604
+ requestId,
605
+ interceptInflight,
606
+ maxInterceptInflight,
607
+ }),
608
+ );
609
+ writeJson(
610
+ res,
611
+ buildListenInterceptCloseoutPayload({
612
+ id: requestId,
613
+ createdMs: startedAtMs,
614
+ model: responseModel,
615
+ }),
616
+ );
617
+ return true;
618
+ }
584
619
  lastAccepted = {
585
620
  fingerprint,
586
621
  startedAtMs,
@@ -600,6 +635,7 @@ export function createEvenAiEndpoint(opts = {}) {
600
635
  }),
601
636
  );
602
637
 
638
+ interceptInflight += 1;
603
639
  void (async () => {
604
640
  try {
605
641
  const dispatchResult = await Promise.resolve(
@@ -610,6 +646,12 @@ export function createEvenAiEndpoint(opts = {}) {
610
646
  source: "hybrid_voice_endpoint",
611
647
  }),
612
648
  );
649
+ const dispatchRunId =
650
+ dispatchResult &&
651
+ typeof dispatchResult.runId === "string" &&
652
+ dispatchResult.runId.trim()
653
+ ? dispatchResult.runId.trim()
654
+ : null;
613
655
  if (emitListenInterceptBroadcast) {
614
656
  try {
615
657
  emitListenInterceptBroadcast({ sessionKey });
@@ -625,12 +667,7 @@ export function createEvenAiEndpoint(opts = {}) {
625
667
  "info",
626
668
  {
627
669
  sessionKey,
628
- runId:
629
- dispatchResult &&
630
- typeof dispatchResult.runId === "string" &&
631
- dispatchResult.runId.trim()
632
- ? dispatchResult.runId.trim()
633
- : undefined,
670
+ runId: dispatchRunId || undefined,
634
671
  },
635
672
  () => ({
636
673
  requestId,
@@ -643,6 +680,16 @@ export function createEvenAiEndpoint(opts = {}) {
643
680
  : null,
644
681
  }),
645
682
  );
683
+
684
+ if (dispatchRunId) {
685
+ try {
686
+ await runWaiter.waitForRun({
687
+ runId: dispatchRunId,
688
+ sessionKey,
689
+ timeoutMs: requestTimeoutMs,
690
+ });
691
+ } catch (_) {}
692
+ }
646
693
  } catch (err) {
647
694
  let cleanupEmitted = false;
648
695
  let cleanupConnectedAppClients = null;
@@ -687,6 +734,8 @@ export function createEvenAiEndpoint(opts = {}) {
687
734
  cleanupError && cleanupError.message ? cleanupError.message : null,
688
735
  }),
689
736
  );
737
+ } finally {
738
+ interceptInflight -= 1;
690
739
  }
691
740
  })();
692
741
 
@@ -789,12 +838,6 @@ export function createEvenAiEndpoint(opts = {}) {
789
838
  startedAtMs,
790
839
  };
791
840
 
792
- // Track the upstream runId so the disconnect handler can cancel the
793
- // pending wait without having to wait for the full request timeout.
794
- // `clientDisconnected` covers the pre-ack window: if the socket closes
795
- // before sendMessage() resolves, activeRunId is still null so cancelRun
796
- // is unreachable; we record the flag and short-circuit waitForRun once
797
- // the ack arrives.
798
841
  let activeRunId = null;
799
842
  let clientDisconnected = false;
800
843
  const onClientDisconnect = () => {
@@ -818,16 +861,11 @@ export function createEvenAiEndpoint(opts = {}) {
818
861
  try {
819
862
  runWaiter.cancelRun(activeRunId, "client_disconnect");
820
863
  } catch (_err) {
821
- // Cancellation is best-effort: the upstream run still completes
822
- // server-side because no gateway abort RPC exists.
864
+
823
865
  }
824
866
  }
825
867
  };
826
- // `res` emits 'close' both when the response finishes normally and when
827
- // the client disconnects mid-flight. We discriminate via
828
- // res.writableEnded inside the handler. (req's own 'close' is not
829
- // reliable for premature client aborts in Node 22 — the socket-level
830
- // close is what fires.)
868
+
831
869
  res.once("close", onClientDisconnect);
832
870
 
833
871
  emitDebug(
@@ -867,14 +905,29 @@ export function createEvenAiEndpoint(opts = {}) {
867
905
  seedFastModeForRoute({ route, sessionKey, routingMode }),
868
906
  );
869
907
  } catch (err) {
870
- // Seed failure must never block the send — the turn just runs
871
- // without fast mode.
908
+
872
909
  emitDebug("evenai", "fast_mode_seed_failed", "warn", { sessionKey }, () => ({
873
910
  requestId,
874
911
  message: err && err.message ? err.message : String(err),
875
912
  }));
876
913
  }
877
914
  }
915
+ if (resolveAgentForRoute) {
916
+ try {
917
+ const agentId = await Promise.resolve(
918
+ resolveAgentForRoute({ route, sessionKey, routingMode }),
919
+ );
920
+ if (typeof agentId === "string" && agentId.trim()) {
921
+ sendOptions.agentId = agentId.trim();
922
+ }
923
+ } catch (err) {
924
+
925
+ emitDebug("evenai", "agent_resolve_failed", "warn", { sessionKey }, () => ({
926
+ requestId,
927
+ message: err && err.message ? err.message : String(err),
928
+ }));
929
+ }
930
+ }
878
931
  const ack = await promiseWithTimeout(
879
932
  gatewayBridge.sendMessage(
880
933
  userText,
@@ -81,7 +81,6 @@ export function createEvenAiRunWaiter(opts = {}) {
81
81
  const clearTimeoutFn =
82
82
  typeof opts.clearTimeout === "function" ? opts.clearTimeout : clearTimeout;
83
83
 
84
- /** @type {Map<string, {resolve: Function, reject: Function, sessionKey: string|null, timer: any, timeoutMs: number|null}>} */
85
84
  const pendingRuns = new Map();
86
85
 
87
86
  function cleanupPending(runId) {
@@ -83,6 +83,10 @@ export function normalizeEvenAiListenEnabled(value) {
83
83
  return value === true;
84
84
  }
85
85
 
86
+ export function normalizeEvenAiDefaultAgent(value) {
87
+ return normalizeTrimmedString(value);
88
+ }
89
+
86
90
  function normalizeTrackedThrowawayKeys(value) {
87
91
  if (!Array.isArray(value)) {
88
92
  return [];
@@ -137,6 +141,9 @@ function isStoredSnapshotCanonical(value, snapshot) {
137
141
  if (normalizeEvenAiDefaultFastMode(value.defaultFastMode) !== snapshot.defaultFastMode) {
138
142
  return false;
139
143
  }
144
+ if (normalizeEvenAiDefaultAgent(value.defaultAgent) !== snapshot.defaultAgent) {
145
+ return false;
146
+ }
140
147
  if (!Array.isArray(value.trackedThrowawayKeys)) {
141
148
  return snapshot.trackedThrowawayKeys.length === 0;
142
149
  }
@@ -155,6 +162,7 @@ export function normalizeEvenAiSettingsSnapshot(value = {}) {
155
162
  defaultThinking: normalizeEvenAiDefaultThinking(value.defaultThinking),
156
163
  listenEnabled: normalizeEvenAiListenEnabled(value.listenEnabled),
157
164
  defaultFastMode: normalizeEvenAiDefaultFastMode(value.defaultFastMode),
165
+ defaultAgent: normalizeEvenAiDefaultAgent(value.defaultAgent),
158
166
  trackedThrowawayKeys: normalizeTrackedThrowawayKeys(value.trackedThrowawayKeys),
159
167
  };
160
168
  }
@@ -364,6 +372,9 @@ export function createEvenAiSettingsStore(opts = {}) {
364
372
  defaultFastMode: hasOwn(patch, "defaultFastMode")
365
373
  ? normalizeEvenAiDefaultFastMode(patch.defaultFastMode)
366
374
  : snapshot.defaultFastMode,
375
+ defaultAgent: hasOwn(patch, "defaultAgent")
376
+ ? normalizeEvenAiDefaultAgent(patch.defaultAgent)
377
+ : snapshot.defaultAgent,
367
378
  trackedThrowawayKeys: [...snapshot.trackedThrowawayKeys],
368
379
  };
369
380
  snapshot = next;
@@ -61,6 +61,14 @@ function buildAgentRequestParams(
61
61
  params.thinking = thinking;
62
62
  }
63
63
 
64
+ const agentId =
65
+ requestOptions && typeof requestOptions.agentId === "string"
66
+ ? requestOptions.agentId.trim()
67
+ : "";
68
+ if (agentId) {
69
+ params.agentId = agentId;
70
+ }
71
+
64
72
  if (
65
73
  attachment &&
66
74
  typeof attachment === "object" &&
@@ -93,15 +101,6 @@ function buildAgentRequestParams(
93
101
  return params;
94
102
  }
95
103
 
96
- /**
97
- * Bridge the relay runtime to OpenClaw structured RPC method calls.
98
- *
99
- * This keeps relay-facing bridge semantics unchanged while switching
100
- * send/approval operations to request-based structured RPC calls.
101
- *
102
- * @param {{openclawClient: object, idempotencyKeyFactory?: () => string}} opts
103
- * @returns {object}
104
- */
105
104
  function createPluginRpcGatewayBridge(opts) {
106
105
  const openclawClient = opts && opts.openclawClient;
107
106
  const idempotencyKeyFactory =
@@ -75,6 +75,7 @@ function sanitizeDiagnostic(args) {
75
75
  pickBoolean(diagnostic.hasAttachment) ?? attachmentSummary.hasAttachment;
76
76
  return {
77
77
  messageId: pickId(diagnostic.messageId ?? args.messageId),
78
+ sessionKey: pickId(diagnostic.sessionKey ?? args.sessionKey),
78
79
  source: pickId(diagnostic.source ?? args.source),
79
80
  textChars,
80
81
  hasAttachment,
@@ -160,10 +161,6 @@ export function createGatewayTimingLedger(opts = {}) {
160
161
  clearTimer(ref);
161
162
  }
162
163
 
163
- // Diagnostic timers must never keep the host process alive: a bare
164
- // 120s ttlTimer kept `node tests/relay.test.js` idling ~120s after the
165
- // suite finished. Injected fake timers (numeric ids) have no unref —
166
- // guard for it.
167
164
  function armDiagnosticTimer(fn, delayMs) {
168
165
  const timer = setTimer(fn, delayMs);
169
166
  if (timer && typeof timer.unref === "function") {
@@ -347,7 +344,9 @@ export function createGatewayTimingLedger(opts = {}) {
347
344
  method: pickId(args.method) || "unknown",
348
345
  expectFinal: args.expectFinal === true,
349
346
  sessionKey: pickId(
350
- args.sessionKey ?? (isObject(args.params) ? args.params.sessionKey : null),
347
+ diagnostic.sessionKey ??
348
+ args.sessionKey ??
349
+ (isObject(args.params) ? args.params.sessionKey : null),
351
350
  ),
352
351
  messageId: diagnostic.messageId,
353
352
  source: diagnostic.source,
@@ -419,7 +418,10 @@ export function createGatewayTimingLedger(opts = {}) {
419
418
  data,
420
419
  );
421
420
 
422
- if (ok && method === "agent" && status === "accepted" && runId) {
421
+ const acceptedAgentRun =
422
+ (method === "agent" && status === "accepted") ||
423
+ (method === "sessions.steer" && (status === "started" || status === "accepted"));
424
+ if (ok && acceptedAgentRun && runId) {
423
425
  const run = {
424
426
  runId,
425
427
  requestId,