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
@@ -0,0 +1,10 @@
1
+ export function sanitizeConnectReason(message) {
2
+ return String(message ?? "")
3
+ .replace(
4
+ /(token|secret|nonce|sig|signature|authorization|password|auth)=([^\s&"']+)/gi,
5
+ "$1=[REDACTED]",
6
+ )
7
+ .replace(/\bbearer\s+\S+/gi, "bearer [REDACTED]")
8
+ .replace(/[A-Za-z0-9_\-+/.=]{40,}/g, "[REDACTED]")
9
+ .slice(0, 300);
10
+ }
@@ -0,0 +1,34 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import { sanitizeConnectReason } from "./sanitize-connect-reason.ts";
5
+
6
+ describe("sanitizeConnectReason", () => {
7
+ it("redacts token= / authorization= key-value secrets", () => {
8
+ assert.equal(
9
+ sanitizeConnectReason("connect ws://h/?token=abc123def failed"),
10
+ "connect ws://h/?token=[REDACTED] failed",
11
+ );
12
+ assert.equal(sanitizeConnectReason("authorization=Bearerxyz"), "authorization=[REDACTED]");
13
+ });
14
+
15
+ it("redacts a bearer credential", () => {
16
+ assert.equal(sanitizeConnectReason("got bearer shorttok here"), "got bearer [REDACTED] here");
17
+ });
18
+
19
+ it("redacts a long credential-shaped run (jwt / signature / nonce)", () => {
20
+ const jwt = "a".repeat(60);
21
+ assert.equal(sanitizeConnectReason(`sig ${jwt}`), "sig [REDACTED]");
22
+ });
23
+
24
+ it("passes a benign reason through unchanged", () => {
25
+ assert.equal(sanitizeConnectReason("Unexpected server response: 1008"), "Unexpected server response: 1008");
26
+ assert.equal(sanitizeConnectReason("first request must be connect"), "first request must be connect");
27
+ });
28
+
29
+ it("handles null/undefined and caps length at 300", () => {
30
+ assert.equal(sanitizeConnectReason(null), "");
31
+ assert.equal(sanitizeConnectReason(undefined), "");
32
+ assert.equal(sanitizeConnectReason("ok ".repeat(200)).length, 300);
33
+ });
34
+ });
package/dist/index.js CHANGED
@@ -64,21 +64,21 @@ export default function register(api) {
64
64
  try {
65
65
  glassesUiDispose();
66
66
  } catch (_) {
67
- /* ignore: dispose is a best-effort cleanup */
67
+
68
68
  }
69
69
  }
70
70
  if (typeof deviceInfoDispose === "function") {
71
71
  try {
72
72
  deviceInfoDispose();
73
73
  } catch (_) {
74
- /* ignore: dispose is a best-effort cleanup */
74
+
75
75
  }
76
76
  }
77
77
  if (typeof distillerDispose === "function") {
78
78
  try {
79
79
  distillerDispose();
80
80
  } catch (_) {
81
- /* ignore: dispose is a best-effort cleanup */
81
+
82
82
  }
83
83
  }
84
84
  return service.stop({ logger: ctx && ctx.logger });
@@ -1,10 +1,5 @@
1
1
  import { composeChannelTwoFragment } from "../domain/prompt-channel-fragments.js";
2
2
 
3
- /**
4
- * Build a before_prompt_build hook that injects the Channel-2 fragment.
5
- * @param {{getDisplayStartStates:Function, getDisplayCurrentStates:Function,
6
- * hasConnectedAppClient:Function}} service
7
- */
8
3
  export function createChannelTwoHook(service, opts = {}) {
9
4
  const emitDebug = typeof opts.emitDebug === "function" ? opts.emitDebug : () => {};
10
5
  return function channelTwoBeforePromptBuild(_event, ctx) {
@@ -27,7 +22,7 @@ export function createChannelTwoHook(service, opts = {}) {
27
22
  { sessionKey }, () => ({ chars: fragment.length }));
28
23
  return { appendSystemContext: fragment };
29
24
  } catch (_err) {
30
- // Defensive: any state-read failure yields no injection.
25
+
31
26
  return undefined;
32
27
  }
33
28
  };
@@ -1,8 +1,5 @@
1
1
  import fs from "node:fs";
2
2
 
3
- // Marker files written by the container runtimes themselves: Docker creates
4
- // /.dockerenv, Podman creates /run/.containerenv. Kubernetes-style runtimes
5
- // leave neither — detection there would need heuristics too fragile to ship.
6
3
  const CONTAINER_MARKER_PATHS = ["/.dockerenv", "/run/.containerenv"];
7
4
 
8
5
  export function isLoopbackBindAddress(address) {
@@ -22,8 +19,7 @@ export function isContainerEnvironment(deps = {}) {
22
19
  try {
23
20
  if (existsSync(markerPath)) return true;
24
21
  } catch {
25
- // No filesystem signal beats a startup failure: treat unreadable markers
26
- // as "not a container" and keep the relay booting.
22
+
27
23
  }
28
24
  }
29
25
  return false;
@@ -0,0 +1,159 @@
1
+ import { assembleBundle, chunkZip } from "../domain/debug-bundle.js";
2
+ import { buildBundlePreview } from "../domain/debug-bundle-preview.js";
3
+
4
+ function computeAvailableSpanMs(dumpResult) {
5
+ const now = dumpResult && typeof dumpResult.nowMs === "number" ? dumpResult.nowMs : 0;
6
+ if (dumpResult && typeof dumpResult.oldestMatchedMs === "number") {
7
+ return Math.max(0, now - dumpResult.oldestMatchedMs);
8
+ }
9
+ const events = dumpResult && dumpResult.events;
10
+ if (!Array.isArray(events) || events.length === 0) return 0;
11
+ let min = Infinity;
12
+ for (const e of events) {
13
+ const ts = e && typeof e.ts === "number" ? e.ts : null;
14
+ if (ts !== null && ts < min) min = ts;
15
+ }
16
+ if (!Number.isFinite(min)) return 0;
17
+ return Math.max(0, now - min);
18
+ }
19
+
20
+ export async function handleDebugBundleRequest(deps, clientId, msg) {
21
+ if (!deps.gatesOn()) {
22
+ deps.emit("capture_refused", { requestId: msg.requestId, reason: "gates_off" });
23
+
24
+ deps.send(clientId, {
25
+ type: "debug-bundle-error",
26
+ requestId: msg.requestId,
27
+ reason: "upload_not_allowed",
28
+ });
29
+ return;
30
+ }
31
+ deps.emit("capture_requested", {
32
+ requestId: msg.requestId,
33
+ redactionMode: msg.redactionMode,
34
+ });
35
+
36
+ const windowMs =
37
+ typeof msg.windowMs === "number" && Number.isFinite(msg.windowMs) && msg.windowMs > 0
38
+ ? Math.floor(msg.windowMs)
39
+ : null;
40
+ const dumpResult = deps.dump(
41
+ windowMs ? { categories: deps.preset, sinceAgeMs: windowMs } : { categories: deps.preset },
42
+ );
43
+
44
+ if (!dumpResult || dumpResult.ok === false) {
45
+ deps.emit("capture_failed", { requestId: msg.requestId, reason: "dump_failed" });
46
+ deps.send(clientId, { type: "debug-bundle-error", requestId: msg.requestId, reason: "dump_failed" });
47
+ return;
48
+ }
49
+ const availableSpanMs = computeAvailableSpanMs(dumpResult);
50
+
51
+ try {
52
+ const bundle = assembleBundle(dumpResult, {
53
+ installId: msg.installId,
54
+ build: deps.build,
55
+ redactionMode: msg.redactionMode || "structural",
56
+ ringCappedWindow: false,
57
+ idSalt: deps.idSalt,
58
+ maxZipBytes: deps.maxZipBytes,
59
+ chunkBytes: deps.chunkBytes,
60
+ note: msg.note,
61
+ });
62
+ deps.emit("bundle_assembled", {
63
+ requestId: msg.requestId,
64
+ categories: bundle.metadata.categories.length,
65
+ totalBytes: bundle.metadata.totalBytes,
66
+ ringCappedWindow: bundle.metadata.window.ringCappedWindow,
67
+ });
68
+ const bundleId = deps.newBundleId();
69
+ deps.cachePut(bundleId, {
70
+ zip: bundle.zip,
71
+ metadataJson: JSON.stringify(bundle.metadata),
72
+ bundleSha256: bundle.bundleSha256,
73
+ cachedMs: deps.now(),
74
+ });
75
+
76
+ const frameMetadataJson = JSON.stringify({
77
+ ...bundle.metadata,
78
+ zipBytes: bundle.zip.length,
79
+ availableSpanMs,
80
+ });
81
+ deps.send(clientId, {
82
+ type: "debug-bundle-meta",
83
+ requestId: msg.requestId,
84
+ bundleId,
85
+ metadataJson: frameMetadataJson,
86
+ });
87
+
88
+ deps.send(clientId, {
89
+ type: "debug-bundle-preview",
90
+ requestId: msg.requestId,
91
+ bundleId,
92
+ sampleJson: JSON.stringify(buildBundlePreview(bundle.files, { maxEvents: 15, maxCharsPerEvent: 80 })),
93
+ });
94
+ deps.emit("bundle_cached", {
95
+ requestId: msg.requestId,
96
+ bundleId,
97
+ parts: bundle.chunks.length,
98
+ });
99
+ } catch (err) {
100
+
101
+ deps.emit("upload_failed", { requestId: msg.requestId, reason: "assembly_failed" });
102
+ deps.send(clientId, { type: "debug-bundle-error", requestId: msg.requestId, reason: "assembly_failed" });
103
+ return;
104
+ }
105
+ }
106
+
107
+ export async function handleDebugBundleSave(deps, clientId, msg) {
108
+ if (!deps.gatesOn()) {
109
+ deps.emit("save_refused", { requestId: msg.requestId, reason: "upload_not_allowed" });
110
+ deps.send(clientId, { type: "debug-bundle-error", requestId: msg.requestId, reason: "upload_not_allowed" });
111
+ return;
112
+ }
113
+ const entry = deps.cacheGet(msg.bundleId);
114
+ if (!entry) {
115
+ deps.emit("save_expired", { requestId: msg.requestId, bundleId: msg.bundleId });
116
+ deps.send(clientId, { type: "debug-bundle-error", requestId: msg.requestId, reason: "bundle_expired" });
117
+ return;
118
+ }
119
+
120
+ const reporterNote = typeof msg.note === "string" ? msg.note : "";
121
+ let sidecarMetadataJson;
122
+ try {
123
+ sidecarMetadataJson = JSON.stringify(
124
+ { ...JSON.parse(entry.metadataJson), reporterNote, reporterRedactionMode: "off" },
125
+ null,
126
+ 2,
127
+ );
128
+ } catch {
129
+ sidecarMetadataJson = entry.metadataJson;
130
+ }
131
+ try {
132
+ const { savedPath, fileSize } = deps.saveBundle({ bundleId: msg.bundleId, savedMs: deps.now(), zip: entry.zip, metadataJson: sidecarMetadataJson });
133
+ deps.emit("bundle_written", { requestId: msg.requestId, bundleId: msg.bundleId, fileSize });
134
+ deps.send(clientId, { type: "debug-bundle-saved", requestId: msg.requestId, bundleId: msg.bundleId, savedPath, fileSize });
135
+ } catch {
136
+ deps.emit("save_failed", { requestId: msg.requestId, reason: "save_failed" });
137
+ deps.send(clientId, { type: "debug-bundle-error", requestId: msg.requestId, reason: "save_failed" });
138
+ }
139
+
140
+ }
141
+
142
+ export async function handleDebugBundleFetch(deps, clientId, msg) {
143
+ if (!deps.gatesOn()) {
144
+ deps.emit("fetch_refused", { requestId: msg.requestId, reason: "upload_not_allowed" });
145
+ deps.send(clientId, { type: "debug-bundle-error", requestId: msg.requestId, reason: "upload_not_allowed" });
146
+ return;
147
+ }
148
+ const entry = deps.cacheGet(msg.bundleId);
149
+ if (!entry) {
150
+ deps.emit("fetch_expired", { requestId: msg.requestId, bundleId: msg.bundleId });
151
+ deps.send(clientId, { type: "debug-bundle-error", requestId: msg.requestId, reason: "bundle_expired" });
152
+ return;
153
+ }
154
+ const chunks = chunkZip(entry.zip, deps.chunkBytes);
155
+ for (const chunk of chunks) {
156
+ deps.send(clientId, { type: "debug-bundle", requestId: msg.requestId, bundleId: msg.bundleId, partIndex: chunk.partIndex, partCount: chunk.partCount, partBase64: chunk.partBase64, bundleSha256: entry.bundleSha256 });
157
+ }
158
+ deps.emit("handoff_complete", { requestId: msg.requestId, bundleId: msg.bundleId, parts: chunks.length });
159
+ }
@@ -4,24 +4,13 @@ import * as path from "node:path";
4
4
  const DEFAULTS = { emoji: false, pace: false };
5
5
  const STORE_FILENAME = "ocuclaw-display-toggles.json";
6
6
 
7
- /**
8
- * Tracks, per session, the display-feature toggle state at session start
9
- * (frozen on first record) and the latest reported state.
10
- *
11
- * The FROZEN start-state is persisted to stateDir so it survives a relay/plugin
12
- * restart — the Channel-1 prompt snapshot is also persisted, so without this the
13
- * Channel-2 disable stop-notice would be lost after a restart (a session that
14
- * started with a feature ON would re-freeze start=OFF from the first post-restart
15
- * send and never notice the disable). The CURRENT state is NOT persisted: it
16
- * re-derives from the next send, so persisting it would mean a write per turn.
17
- */
18
7
  export function createDisplayToggleTracker(opts = {}) {
19
8
  const limit = Number.isFinite(opts.limit) ? opts.limit : 200;
20
9
  const statePath =
21
10
  typeof opts.stateDir === "string" && opts.stateDir.trim()
22
11
  ? path.join(opts.stateDir.trim(), STORE_FILENAME)
23
12
  : null;
24
- /** @type {Map<string,{start:{emoji,pace},current:{emoji,pace}}>} */
13
+
25
14
  const byKey = new Map();
26
15
 
27
16
  function norm(v) {
@@ -36,13 +25,13 @@ export function createDisplayToggleTracker(opts = {}) {
36
25
  for (const [k, v] of Object.entries(parsed.entries)) {
37
26
  if (v && v.start) {
38
27
  const start = norm(v.start);
39
- // current re-derives on the next send; seed it from start until then.
28
+
40
29
  byKey.set(k, { start, current: { ...start } });
41
30
  }
42
31
  }
43
32
  }
44
33
  } catch (_e) {
45
- // Missing/corrupt store is non-fatal: start empty.
34
+
46
35
  }
47
36
  }
48
37
 
@@ -55,7 +44,7 @@ export function createDisplayToggleTracker(opts = {}) {
55
44
  fs.writeFileSync(tmp, JSON.stringify({ version: 1, entries }), { mode: 0o600 });
56
45
  fs.renameSync(tmp, statePath);
57
46
  } catch (_e) {
58
- // Best-effort persistence.
47
+
59
48
  }
60
49
  }
61
50
 
@@ -76,10 +65,10 @@ export function createDisplayToggleTracker(opts = {}) {
76
65
  if (!existing) {
77
66
  byKey.set(sessionKey, { start: cur, current: cur });
78
67
  evictIfNeeded();
79
- persist(); // a new frozen start-state was recorded
68
+ persist();
80
69
  return;
81
70
  }
82
- existing.current = cur; // start stays frozen; no persist (start unchanged)
71
+ existing.current = cur;
83
72
  },
84
73
  getStart(sessionKey) {
85
74
  const e = byKey.get(sessionKey);