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
@@ -0,0 +1,68 @@
1
+ const DEFAULT_TTL_MS = 30_000;
2
+ const DEFAULT_MAX_ENTRIES = 64;
3
+
4
+ function normalizeNonNegativeInteger(value, fallback) {
5
+ if (!Number.isFinite(Number(value))) return fallback;
6
+ return Math.max(0, Math.floor(Number(value)));
7
+ }
8
+
9
+ export function createApprovalReplayCache(options = {}) {
10
+ const now = typeof options.now === "function" ? options.now : () => Date.now();
11
+ const ttlMs = normalizeNonNegativeInteger(options.ttlMs, DEFAULT_TTL_MS);
12
+ const maxEntries = normalizeNonNegativeInteger(options.maxEntries, DEFAULT_MAX_ENTRIES);
13
+ const entries = new Map(); // id -> { frame, cachedAtMs, frameExpiresAtMs }
14
+
15
+ function nowMs() {
16
+ return normalizeNonNegativeInteger(now(), Date.now());
17
+ }
18
+
19
+ function isStale(entry, atMs) {
20
+ if (ttlMs > 0 && atMs - entry.cachedAtMs > ttlMs) return true;
21
+ if (entry.frameExpiresAtMs > 0 && entry.frameExpiresAtMs <= atMs) return true;
22
+ return false;
23
+ }
24
+
25
+ function set(id, frame, frameExpiresAtMs) {
26
+ if (typeof id !== "string" || !id.trim()) return;
27
+ const key = id.trim();
28
+ entries.delete(key);
29
+ entries.set(key, {
30
+ frame,
31
+ cachedAtMs: nowMs(),
32
+ frameExpiresAtMs: normalizeNonNegativeInteger(frameExpiresAtMs, 0),
33
+ });
34
+ while (maxEntries > 0 && entries.size > maxEntries) {
35
+ const oldest = entries.keys().next();
36
+ if (oldest.done) break;
37
+ entries.delete(oldest.value);
38
+ }
39
+ }
40
+
41
+ function remove(id) {
42
+ if (typeof id !== "string") return false;
43
+ return entries.delete(id.trim());
44
+ }
45
+
46
+ function activeFrames() {
47
+ const atMs = nowMs();
48
+ const frames = [];
49
+ for (const [key, entry] of entries) {
50
+ if (isStale(entry, atMs)) {
51
+ entries.delete(key);
52
+ continue;
53
+ }
54
+ frames.push(entry.frame);
55
+ }
56
+ return frames;
57
+ }
58
+
59
+ function size() {
60
+ return entries.size;
61
+ }
62
+
63
+ function clear() {
64
+ entries.clear();
65
+ }
66
+
67
+ return { set, remove, activeFrames, size, clear };
68
+ }
@@ -0,0 +1,32 @@
1
+ import { parentPort } from "node:worker_threads";
2
+ import { createRelayWorkerTransport } from "./relay-worker-transport.js";
3
+
4
+ if (!parentPort) {
5
+ throw new Error("relay worker entry requires parentPort");
6
+ }
7
+
8
+ const transport = createRelayWorkerTransport({
9
+ postToMain(message) {
10
+ parentPort.postMessage(message);
11
+ },
12
+ });
13
+
14
+ parentPort.on("message", async (message) => {
15
+ try {
16
+ if (message && message.kind === "manifest") {
17
+ await transport.start(message);
18
+ return;
19
+ }
20
+ if (message && message.kind === "shutdown") {
21
+ await transport.close();
22
+ parentPort.postMessage({ kind: "worker.closed" });
23
+ return;
24
+ }
25
+ transport.handleMainMessage(message);
26
+ } catch (err) {
27
+ parentPort.postMessage({
28
+ kind: "worker.error",
29
+ message: err && err.message ? err.message : String(err),
30
+ });
31
+ }
32
+ });
@@ -0,0 +1,272 @@
1
+ import { formatWorkerHealth } from "./relay-worker-protocol.js";
2
+
3
+ const DEFAULT_THRESHOLDS = Object.freeze({
4
+ mainDelayedThresholdMs: 2_000,
5
+ mainRecoveredThresholdMs: 800,
6
+ emitHeartbeatMs: 5_000,
7
+ loopLagDegradedP95Ms: 250,
8
+ loopLagRecoveredP95Ms: 100,
9
+ });
10
+
11
+ const MESSAGE_SEND_DEGRADED_DEPTH = 32;
12
+
13
+ function normalizeNonNegativeInteger(value, fallback) {
14
+ if (!Number.isFinite(Number(value))) return fallback;
15
+ return Math.max(0, Math.floor(Number(value)));
16
+ }
17
+
18
+ function normalizeOptionalNonNegativeInteger(value) {
19
+ if (value === null || value === undefined) return null;
20
+ if (!Number.isFinite(Number(value))) return null;
21
+ return Math.max(0, Math.floor(Number(value)));
22
+ }
23
+
24
+ function normalizeThresholds(value = {}) {
25
+ return {
26
+ mainDelayedThresholdMs: normalizeNonNegativeInteger(
27
+ value.mainDelayedThresholdMs,
28
+ DEFAULT_THRESHOLDS.mainDelayedThresholdMs,
29
+ ),
30
+ mainRecoveredThresholdMs: normalizeNonNegativeInteger(
31
+ value.mainRecoveredThresholdMs,
32
+ DEFAULT_THRESHOLDS.mainRecoveredThresholdMs,
33
+ ),
34
+ emitHeartbeatMs: normalizeNonNegativeInteger(
35
+ value.emitHeartbeatMs,
36
+ DEFAULT_THRESHOLDS.emitHeartbeatMs,
37
+ ),
38
+ loopLagDegradedP95Ms: normalizeNonNegativeInteger(
39
+ value.loopLagDegradedP95Ms,
40
+ DEFAULT_THRESHOLDS.loopLagDegradedP95Ms,
41
+ ),
42
+ loopLagRecoveredP95Ms: normalizeNonNegativeInteger(
43
+ value.loopLagRecoveredP95Ms,
44
+ DEFAULT_THRESHOLDS.loopLagRecoveredP95Ms,
45
+ ),
46
+ };
47
+ }
48
+
49
+ function normalizeQueueDepthByClass(value) {
50
+ return {
51
+ "message.send": normalizeNonNegativeInteger(
52
+ value && value["message.send"],
53
+ 0,
54
+ ),
55
+ };
56
+ }
57
+
58
+ export function createRelayWorkerHealthMonitor(options = {}) {
59
+ const now = typeof options.now === "function" ? options.now : () => Date.now();
60
+ const emitFrame = typeof options.emitFrame === "function" ? options.emitFrame : () => {};
61
+ const emitDebug = typeof options.emitDebug === "function" ? options.emitDebug : () => {};
62
+ const workerEpoch = normalizeNonNegativeInteger(options.workerEpoch, 0);
63
+ const thresholds = normalizeThresholds(options.thresholds);
64
+
65
+ let status = "main_disconnected";
66
+ let lastStatusEmitAtMs = null;
67
+ let lastMainHeartbeatAtMs = null;
68
+ let lastMainFrameAtMs = null;
69
+ let lastMainStatusAtMs = null;
70
+ let cachedPagesRevision = null;
71
+ let cachedStatusRevision = null;
72
+ let workerMainQueueDepth = 0;
73
+ let workerMainPostLatencyMs = null;
74
+ let workerQueueDepthByClass = normalizeQueueDepthByClass(null);
75
+ let loopLagP95Ms = null;
76
+ let sendBufferHighWaterClients = 0;
77
+
78
+ function nowMs() {
79
+ return normalizeNonNegativeInteger(now(), Date.now());
80
+ }
81
+
82
+ function ageFrom(atMs, currentMs) {
83
+ if (atMs === null) return null;
84
+ return Math.max(0, currentMs - atMs);
85
+ }
86
+
87
+ function latestMainActivityAtMs() {
88
+ if (lastMainHeartbeatAtMs === null) return lastMainFrameAtMs;
89
+ if (lastMainFrameAtMs === null) return lastMainHeartbeatAtMs;
90
+ return Math.max(lastMainHeartbeatAtMs, lastMainFrameAtMs);
91
+ }
92
+
93
+ function updateCachedRevisions(data = {}, atMs = nowMs()) {
94
+ const pagesRevision = normalizeOptionalNonNegativeInteger(data.cachedPagesRevision);
95
+ const statusRevision = normalizeOptionalNonNegativeInteger(data.cachedStatusRevision);
96
+ if (pagesRevision !== null) {
97
+ cachedPagesRevision = pagesRevision;
98
+ }
99
+ if (statusRevision !== null) {
100
+ cachedStatusRevision = statusRevision;
101
+ lastMainStatusAtMs = normalizeOptionalNonNegativeInteger(data.lastMainStatusAtMs) ?? atMs;
102
+ } else if (data.lastMainStatusAtMs !== undefined) {
103
+ lastMainStatusAtMs = normalizeOptionalNonNegativeInteger(data.lastMainStatusAtMs);
104
+ }
105
+ }
106
+
107
+ function buildFrame(nextStatus) {
108
+ const atMs = nowMs();
109
+ return formatWorkerHealth({
110
+ workerEpoch,
111
+ workerStatus: nextStatus,
112
+ mainFrameAgeMs: ageFrom(lastMainFrameAtMs, atMs),
113
+ mainHeartbeatAgeMs: ageFrom(lastMainHeartbeatAtMs, atMs),
114
+ workerMainQueueDepth,
115
+ workerMainPostLatencyMs,
116
+ workerQueueDepthByClass,
117
+ cachedPagesRevision,
118
+ cachedStatusRevision,
119
+ lastMainStatusAtMs,
120
+ backpressure: {
121
+ loopLagP95Ms,
122
+ sendBufferHighWaterClients,
123
+ messageSendQueueDepth: workerQueueDepthByClass["message.send"],
124
+ },
125
+ });
126
+ }
127
+
128
+ function shouldEmitHeartbeat(atMs) {
129
+ if (lastStatusEmitAtMs === null) return true;
130
+ return thresholds.emitHeartbeatMs > 0 &&
131
+ atMs - lastStatusEmitAtMs >= thresholds.emitHeartbeatMs;
132
+ }
133
+
134
+ function setStatus(nextStatus, force = false) {
135
+ const atMs = nowMs();
136
+ const previousStatus = status;
137
+ const transitioned = nextStatus !== previousStatus;
138
+
139
+ if (!transitioned && !force && !shouldEmitHeartbeat(atMs)) {
140
+ return status;
141
+ }
142
+
143
+ status = nextStatus;
144
+ emitFrame(buildFrame(nextStatus));
145
+ lastStatusEmitAtMs = atMs;
146
+
147
+ if (transitioned) {
148
+ emitDebug("worker_health_transition", "info", {
149
+ from: previousStatus,
150
+ to: nextStatus,
151
+ workerEpoch,
152
+ mainFrameAgeMs: ageFrom(lastMainFrameAtMs, atMs),
153
+ mainHeartbeatAgeMs: ageFrom(lastMainHeartbeatAtMs, atMs),
154
+ workerMainQueueDepth,
155
+ workerQueueDepthByClass,
156
+ });
157
+ }
158
+
159
+ return status;
160
+ }
161
+
162
+ function isDegraded() {
163
+ if (workerQueueDepthByClass["message.send"] >= MESSAGE_SEND_DEGRADED_DEPTH) {
164
+ return true;
165
+ }
166
+ if (sendBufferHighWaterClients >= 1) {
167
+ return true;
168
+ }
169
+ if (loopLagP95Ms !== null) {
170
+ // Hysteresis: enter at the degraded threshold; once degraded, stay degraded
171
+ // until lag drops below the (lower) recovered threshold.
172
+ const enterThreshold =
173
+ status === "degraded"
174
+ ? thresholds.loopLagRecoveredP95Ms
175
+ : thresholds.loopLagDegradedP95Ms;
176
+ if (loopLagP95Ms >= enterThreshold) return true;
177
+ }
178
+ return false;
179
+ }
180
+
181
+ function computeStatus() {
182
+ // Precedence is deliberate: `degraded` (worker strain) is reported before
183
+ // `main_disconnected`. On a live worker the two cannot co-occur — live
184
+ // message.send queue depth tops at 31 (enqueue rejects at the cap before set)
185
+ // and `main_disconnected` is only the pre-first-heartbeat boot state — so this
186
+ // order is never observably wrong. Any future reorder is conditional on BOTH a
187
+ // product-copy reason to prefer a "waiting" label during a disconnect AND a
188
+ // degraded condition becoming live-reachable, and must ship with a degraded
189
+ // secondary-text line + distinct badge in the phone UI.
190
+ if (isDegraded()) {
191
+ return "degraded";
192
+ }
193
+ const mainActivityAtMs = latestMainActivityAtMs();
194
+ if (mainActivityAtMs === null) {
195
+ return "main_disconnected";
196
+ }
197
+
198
+ const mainActivityAgeMs = ageFrom(mainActivityAtMs, nowMs());
199
+ if (status === "main_delayed") {
200
+ return mainActivityAgeMs > thresholds.mainRecoveredThresholdMs
201
+ ? "main_delayed"
202
+ : "ready";
203
+ }
204
+ return mainActivityAgeMs >= thresholds.mainDelayedThresholdMs
205
+ ? "main_delayed"
206
+ : "ready";
207
+ }
208
+
209
+ function recordMainHeartbeat(data = {}) {
210
+ const atMs = nowMs();
211
+ lastMainHeartbeatAtMs = atMs;
212
+ updateCachedRevisions(data, atMs);
213
+ return setStatus(computeStatus(), true);
214
+ }
215
+
216
+ function recordMainFrame(data = {}) {
217
+ const atMs = nowMs();
218
+ lastMainFrameAtMs = atMs;
219
+ updateCachedRevisions(data, atMs);
220
+ return setStatus(computeStatus());
221
+ }
222
+
223
+ function updateQueueDepth(nextDepthByClass = {}) {
224
+ workerQueueDepthByClass = normalizeQueueDepthByClass(nextDepthByClass);
225
+ return status;
226
+ }
227
+
228
+ function updateLoopLagP95Ms(value) {
229
+ loopLagP95Ms = normalizeOptionalNonNegativeInteger(value);
230
+ return status;
231
+ }
232
+
233
+ function updateSendBufferHighWaterClients(value) {
234
+ sendBufferHighWaterClients = normalizeNonNegativeInteger(value, 0);
235
+ return status;
236
+ }
237
+
238
+ function updateWorkerMainQueueDepth(value) {
239
+ workerMainQueueDepth = normalizeNonNegativeInteger(value, 0);
240
+ return status;
241
+ }
242
+
243
+ function updateWorkerMainPostLatencyMs(value) {
244
+ workerMainPostLatencyMs = normalizeOptionalNonNegativeInteger(value);
245
+ return status;
246
+ }
247
+
248
+ function markRestarting() {
249
+ return setStatus("restarting", true);
250
+ }
251
+
252
+ function sample() {
253
+ return setStatus(computeStatus());
254
+ }
255
+
256
+ function currentStatus() {
257
+ return status;
258
+ }
259
+
260
+ return {
261
+ recordMainHeartbeat,
262
+ recordMainFrame,
263
+ updateQueueDepth,
264
+ updateLoopLagP95Ms,
265
+ updateSendBufferHighWaterClients,
266
+ updateWorkerMainQueueDepth,
267
+ updateWorkerMainPostLatencyMs,
268
+ markRestarting,
269
+ sample,
270
+ currentStatus,
271
+ };
272
+ }
@@ -0,0 +1,281 @@
1
+ export const APP_PROTOCOL = Object.freeze({
2
+ messageSend: "ocuclaw.message.send",
3
+ messageSendAck: "ocuclaw.message.send.ack",
4
+ operationReceived: "ocuclaw.operation.received",
5
+ workerOperationReceived: "ocuclaw.worker.operation.received",
6
+ workerHealth: "ocuclaw.worker.health",
7
+ protocolHelloAck: "protocolHelloAck",
8
+ resumeAck: "ocuclaw.sync.resume.ack",
9
+ pages: "ocuclaw.view.pages.snapshot",
10
+ status: "ocuclaw.runtime.status",
11
+ debugConfigSnapshot: "ocuclaw.debug.config.snapshot",
12
+ avatarFetch: "ocuclaw.avatar.fetch",
13
+ avatarBlob: "ocuclaw.avatar.blob",
14
+ readinessSnapshot: "ocuclaw.readiness.snapshot",
15
+ readinessProbeRequest: "ocuclaw.readiness.probe.request",
16
+ readinessProbeAck: "ocuclaw.readiness.probe.ack",
17
+ automationStateGet: "ocuclaw.automation.state.get",
18
+ automationStateSnapshot: "ocuclaw.automation.state.snapshot",
19
+ approvalRequest: "ocuclaw.approval.request",
20
+ approvalResolved: "ocuclaw.approval.resolved",
21
+ sessionContextSnapshot: "ocuclaw.session.context.snapshot",
22
+ sessionCompact: "ocuclaw.session.compact",
23
+ sessionCompactAck: "ocuclaw.session.compact.ack",
24
+ visibility: "visibility",
25
+ });
26
+
27
+ export const WORKER_FEATURES = Object.freeze([
28
+ "worker-health",
29
+ "worker-receipts",
30
+ "worker-resume-metadata",
31
+ "message-send-worker-queue",
32
+ ]);
33
+
34
+ export const DEFAULT_WORKER_QUEUE_CAPS = Object.freeze({
35
+ messageSendMaxEntries: 32,
36
+ messageSendTtlMs: 30_000,
37
+ retainedFinalTtlMs: 30_000,
38
+ oldEpochPendingHardCapMs: 90_000,
39
+ });
40
+
41
+ export const DEFAULT_WORKER_HEALTH_THRESHOLDS = Object.freeze({
42
+ mainDelayedThresholdMs: 2_000,
43
+ mainRecoveredThresholdMs: 800,
44
+ mainStaleResumeThresholdMs: 5_000,
45
+ heartbeatIntervalMs: 1_000,
46
+ emitHeartbeatMs: 5_000,
47
+ loopLagDegradedP95Ms: 250,
48
+ loopLagRecoveredP95Ms: 100,
49
+ });
50
+
51
+ export const DEFAULT_NUDGE_THRESHOLDS = Object.freeze({
52
+ nudgeActiveIntervalMs: 150,
53
+ nudgeSlowIntervalMs: 1000,
54
+ nudgeIdleDeactivateMs: 5000,
55
+ nudgeHeartbeatIntervalMs: 10000,
56
+ nudgeHardTimeoutMs: 60000,
57
+ });
58
+
59
+ export const DEFAULT_WORKER_RPC_LIMITS = Object.freeze({
60
+ mainRequestTimeoutMs: 5_000,
61
+ httpRequestTimeoutMs: 60_000,
62
+ httpMaxBodyBytes: 65_536,
63
+ httpMaxResponseBytes: 262_144,
64
+ });
65
+
66
+ const ALLOWED_WORKER_STATUSES = new Set([
67
+ "ready",
68
+ "main_delayed",
69
+ "main_disconnected",
70
+ "restarting",
71
+ "degraded",
72
+ ]);
73
+ const ALLOWED_CACHE_STATES = new Set(["fresh", "stale", "empty", "warming"]);
74
+ const WORKER_QUEUE_CLASSES = Object.freeze(["message.send"]);
75
+
76
+ export function normalizeRequestId(value) {
77
+ if (typeof value !== "string") return null;
78
+ const trimmed = value.trim();
79
+ return trimmed ? trimmed : null;
80
+ }
81
+
82
+ export function parseNonNegativeRevision(value) {
83
+ if (value === null || value === undefined) return null;
84
+ if (typeof value === "string" && value.trim() === "") return null;
85
+ if (!Number.isFinite(Number(value))) return null;
86
+ const num = Math.floor(Number(value));
87
+ return num >= 0 ? num : null;
88
+ }
89
+
90
+ function parseNonNegativeInteger(value) {
91
+ return parseNonNegativeRevision(value);
92
+ }
93
+
94
+ function parseNonNegativeDuration(value) {
95
+ if (value === null || value === undefined) return null;
96
+ if (typeof value === "string" && value.trim() === "") return null;
97
+ if (!Number.isFinite(Number(value))) return null;
98
+ return Math.max(0, Math.floor(Number(value)));
99
+ }
100
+
101
+ function normalizeWorkerStatus(value) {
102
+ return ALLOWED_WORKER_STATUSES.has(value) ? value : "ready";
103
+ }
104
+
105
+ function normalizeCacheState(value) {
106
+ return ALLOWED_CACHE_STATES.has(value) ? value : null;
107
+ }
108
+
109
+ function normalizeWorkerQueueDepthByClass(value) {
110
+ const source = value && typeof value === "object" && !Array.isArray(value)
111
+ ? value
112
+ : {};
113
+ const depths = {};
114
+ for (const className of WORKER_QUEUE_CLASSES) {
115
+ const parsed = parseNonNegativeInteger(source[className]);
116
+ depths[className] = parsed === null ? 0 : parsed;
117
+ }
118
+ return depths;
119
+ }
120
+
121
+ function normalizeRequestIdList(value) {
122
+ if (!Array.isArray(value)) return null;
123
+ const ids = [];
124
+ for (const entry of value) {
125
+ const requestId = normalizeRequestId(entry);
126
+ if (requestId) {
127
+ ids.push(requestId);
128
+ }
129
+ }
130
+ return ids;
131
+ }
132
+
133
+ export function estimateJsonByteLength(value) {
134
+ return Buffer.byteLength(JSON.stringify(value ?? null), "utf8");
135
+ }
136
+
137
+ export function parseMessageType(message) {
138
+ try {
139
+ const parsed = typeof message === "string" ? JSON.parse(message) : message;
140
+ return parsed && typeof parsed.type === "string" ? parsed.type : null;
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+
146
+ function normalizeBackpressure(value) {
147
+ const v = value && typeof value === "object" ? value : {};
148
+ return {
149
+ loopLagP95Ms: parseNonNegativeDuration(v.loopLagP95Ms),
150
+ sendBufferHighWaterClients: parseNonNegativeInteger(v.sendBufferHighWaterClients) ?? 0,
151
+ messageSendQueueDepth: parseNonNegativeInteger(v.messageSendQueueDepth) ?? 0,
152
+ };
153
+ }
154
+
155
+ export function formatWorkerHealth(data = {}) {
156
+ return JSON.stringify({
157
+ type: APP_PROTOCOL.workerHealth,
158
+ workerEpoch: parseNonNegativeInteger(data.workerEpoch) ?? 0,
159
+ workerStatus: normalizeWorkerStatus(data.workerStatus),
160
+ mainFrameAgeMs: parseNonNegativeDuration(data.mainFrameAgeMs),
161
+ mainHeartbeatAgeMs: parseNonNegativeDuration(data.mainHeartbeatAgeMs),
162
+ workerMainQueueDepth: parseNonNegativeInteger(data.workerMainQueueDepth) ?? 0,
163
+ workerMainPostLatencyMs: parseNonNegativeDuration(data.workerMainPostLatencyMs),
164
+ workerQueueDepthByClass: normalizeWorkerQueueDepthByClass(data.workerQueueDepthByClass),
165
+ cachedPagesRevision: parseNonNegativeRevision(data.cachedPagesRevision),
166
+ cachedStatusRevision: parseNonNegativeRevision(data.cachedStatusRevision),
167
+ lastMainStatusAtMs: parseNonNegativeInteger(data.lastMainStatusAtMs),
168
+ backpressure: normalizeBackpressure(data.backpressure),
169
+ });
170
+ }
171
+
172
+ export function formatWorkerOperationReceived(data) {
173
+ const requestId = normalizeRequestId(data && data.requestId);
174
+ if (!requestId) throw new Error("worker receipt requires requestId");
175
+ return JSON.stringify({
176
+ type: APP_PROTOCOL.workerOperationReceived,
177
+ requestId,
178
+ operation: data.operation || "message.send",
179
+ status: "worker_pending",
180
+ phase: "worker_received",
181
+ workerEpoch: parseNonNegativeInteger(data.workerEpoch) ?? 0,
182
+ receivedAtMs: parseNonNegativeInteger(data.receivedAtMs) ?? Date.now(),
183
+ });
184
+ }
185
+
186
+ export function formatMainOperationReceived(data) {
187
+ const requestId = normalizeRequestId(data && data.requestId);
188
+ if (!requestId) throw new Error("main receipt requires requestId");
189
+ return JSON.stringify({
190
+ type: APP_PROTOCOL.operationReceived,
191
+ requestId,
192
+ operation: data.operation || "message.send",
193
+ status: data.status || "upstream_pending",
194
+ phase: data.phase || "relay_received",
195
+ receivedAtMs: parseNonNegativeInteger(data.receivedAtMs) ?? Date.now(),
196
+ });
197
+ }
198
+
199
+ export function formatSendAck(requestId, status, error, errorCode) {
200
+ const id = normalizeRequestId(requestId);
201
+ const msg = { type: APP_PROTOCOL.messageSendAck, requestId: id, status };
202
+ if (error !== undefined) msg.error = error;
203
+ if (errorCode !== undefined) msg.errorCode = errorCode;
204
+ return JSON.stringify(msg);
205
+ }
206
+
207
+ export function formatWorkerQueueTimeoutAck(requestId) {
208
+ return formatSendAck(
209
+ requestId,
210
+ "rejected",
211
+ "OpenClaw did not accept the message before the relay worker queue timeout.",
212
+ "worker_queue_timeout",
213
+ );
214
+ }
215
+
216
+ export function formatWorkerRestartUncertainAck(requestId) {
217
+ return formatSendAck(
218
+ requestId,
219
+ "rejected",
220
+ "The relay worker restarted before OpenClaw accepted the message. Retry may resend the message.",
221
+ "worker_restarted_before_main_accept",
222
+ );
223
+ }
224
+
225
+ export function formatProtocolHelloAck(payload = {}) {
226
+ const ack = {
227
+ type: APP_PROTOCOL.protocolHelloAck,
228
+ protocolVersion: payload.protocolVersion || "v2",
229
+ supportedProtocolVersions: Array.isArray(payload.supportedProtocolVersions)
230
+ ? payload.supportedProtocolVersions
231
+ : ["v2"],
232
+ reason: payload.reason || null,
233
+ deprecatedV1: false,
234
+ };
235
+ if (typeof payload.pluginVersion === "string" && payload.pluginVersion) ack.pluginVersion = payload.pluginVersion;
236
+ if (typeof payload.requiresClientVersion === "string" && payload.requiresClientVersion) ack.requiresClientVersion = payload.requiresClientVersion;
237
+ if (typeof payload.pluginId === "string" && payload.pluginId) ack.pluginId = payload.pluginId;
238
+ const workerEpoch = parseNonNegativeInteger(payload.workerEpoch);
239
+ if (workerEpoch !== null) ack.workerEpoch = workerEpoch;
240
+ if (Array.isArray(payload.workerFeatures)) ack.workerFeatures = payload.workerFeatures;
241
+ return JSON.stringify(ack);
242
+ }
243
+
244
+ export function formatResumeAck(payload = {}) {
245
+ const msg = {
246
+ type: APP_PROTOCOL.resumeAck,
247
+ reason: payload.reason || null,
248
+ sentPages: !!payload.sentPages,
249
+ sentStatus: !!payload.sentStatus,
250
+ sentApprovals: parseNonNegativeInteger(payload.sentApprovals) ?? 0,
251
+ pagesRevision: parseNonNegativeRevision(payload.pagesRevision),
252
+ statusRevision: parseNonNegativeRevision(payload.statusRevision),
253
+ };
254
+ for (const key of [
255
+ "workerEpoch",
256
+ "previousWorkerEpoch",
257
+ "cachedPagesRevision",
258
+ "cachedStatusRevision",
259
+ ]) {
260
+ const parsed = parseNonNegativeRevision(payload[key]);
261
+ if (parsed !== null) msg[key] = parsed;
262
+ }
263
+ for (const key of ["workerRestarted", "mainStale", "resumeProvisional"]) {
264
+ if (payload[key] !== undefined) msg[key] = !!payload[key];
265
+ }
266
+ const cacheState = normalizeCacheState(payload.cacheState);
267
+ if (cacheState) msg.cacheState = cacheState;
268
+ const workerOnlyPendingRequestIds = normalizeRequestIdList(
269
+ payload.workerOnlyPendingRequestIds,
270
+ );
271
+ if (workerOnlyPendingRequestIds) {
272
+ msg.workerOnlyPendingRequestIds = workerOnlyPendingRequestIds;
273
+ }
274
+ const unresolvedWorkerPendingRequestIds = normalizeRequestIdList(
275
+ payload.unresolvedWorkerPendingRequestIds,
276
+ );
277
+ if (unresolvedWorkerPendingRequestIds) {
278
+ msg.unresolvedWorkerPendingRequestIds = unresolvedWorkerPendingRequestIds;
279
+ }
280
+ return JSON.stringify(msg);
281
+ }