ocuclaw 1.2.4 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +18 -5
  2. package/dist/config/runtime-config.js +81 -3
  3. package/dist/domain/activity-status-adapter.js +138 -605
  4. package/dist/domain/activity-status-arbiter.js +109 -0
  5. package/dist/domain/activity-status-labels.js +906 -0
  6. package/dist/domain/code-span-regions.js +103 -0
  7. package/dist/domain/conversation-state.js +14 -1
  8. package/dist/domain/debug-store.js +38 -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/downstream-server.js +700 -534
  28. package/dist/runtime/ocuclaw-settings-store.js +74 -31
  29. package/dist/runtime/plugin-update-service.js +216 -0
  30. package/dist/runtime/protocol-adapter.js +9 -0
  31. package/dist/runtime/provider-usage-select.js +168 -0
  32. package/dist/runtime/relay-client-nudge-controller.js +553 -0
  33. package/dist/runtime/relay-core.js +1209 -204
  34. package/dist/runtime/relay-health-monitor.js +172 -0
  35. package/dist/runtime/relay-operation-registry.js +263 -0
  36. package/dist/runtime/relay-service.js +201 -1
  37. package/dist/runtime/relay-worker-approval-replay-cache.js +68 -0
  38. package/dist/runtime/relay-worker-entry.js +32 -0
  39. package/dist/runtime/relay-worker-health.js +272 -0
  40. package/dist/runtime/relay-worker-protocol.js +285 -0
  41. package/dist/runtime/relay-worker-queue.js +202 -0
  42. package/dist/runtime/relay-worker-supervisor.js +1081 -0
  43. package/dist/runtime/relay-worker-transport.js +1051 -0
  44. package/dist/runtime/session-context-service.js +189 -0
  45. package/dist/runtime/session-service.js +615 -24
  46. package/dist/runtime/upstream-runtime.js +1167 -60
  47. package/dist/tools/device-info-tool.js +242 -0
  48. package/dist/tools/glasses-ui-cron.js +427 -0
  49. package/dist/tools/glasses-ui-descriptors.js +261 -0
  50. package/dist/tools/glasses-ui-limits.js +21 -0
  51. package/dist/tools/glasses-ui-paint-floor.js +99 -0
  52. package/dist/tools/glasses-ui-recipes.js +746 -0
  53. package/dist/tools/glasses-ui-surfaces.js +278 -0
  54. package/dist/tools/glasses-ui-template.js +182 -0
  55. package/dist/tools/glasses-ui-tool.js +1147 -0
  56. package/dist/tools/session-title-tool.js +209 -0
  57. package/dist/version.js +2 -0
  58. package/openclaw.plugin.json +163 -15
  59. package/package.json +12 -4
@@ -0,0 +1,261 @@
1
+ // Kind-descriptor registry for the glasses-UI surface kinds. Each descriptor
2
+ // is a self-contained unit owning everything per-kind on the PLUGIN side:
3
+ // - schemaBranch: the JSON-Schema oneOf entry for this kind
4
+ // - validateSpec: per-kind validation, returns the canonical
5
+ // { ok:true, spec } | { ok:false, code, message } shape
6
+ // - refreshTargets: which refresh.targets fields this kind binds
7
+ // ("body" for text, "items" for the list kinds) —
8
+ // informational in Phase 1, consumed in Phase 3.
9
+ // The core (glassesUiParametersSchema, validateGlassesUiSpec) dispatches by
10
+ // kind STRING through this registry and never switches on a specific kind, so
11
+ // adding a kind is one descriptor with zero core edits (spec §Modularity).
12
+ // Phase 1 ports the existing 3 kinds with NO behavior change.
13
+ //
14
+ // NOTE: this module imports GLASSES_UI_LIMITS from ./glasses-ui-limits.js (a
15
+ // dependency-free leaf), NOT from ./glasses-ui-tool.js — glasses-ui-tool.js
16
+ // imports THIS module, so importing back from it would form a require cycle
17
+ // that the CJS emitter cannot resolve (module.exports is written at
18
+ // end-of-module, so a mid-cycle require sees {}). See glasses-ui-limits.ts.
19
+
20
+ import { GLASSES_UI_LIMITS } from "./glasses-ui-limits.js";
21
+
22
+ // Shared title validation (moved from validateGlassesUiSpec — every kind ran
23
+ // it before its branch). Returns an error result or null.
24
+ function validateTitle(obj) {
25
+ if (typeof obj.title === "undefined") return null;
26
+ if (typeof obj.title !== "string") {
27
+ return { ok: false, code: "title_too_long", message: "title must be a string" };
28
+ }
29
+ if (obj.title.length > GLASSES_UI_LIMITS.titleMax) {
30
+ return {
31
+ ok: false,
32
+ code: "title_too_long",
33
+ message: `title is ${obj.title.length} chars; max ${GLASSES_UI_LIMITS.titleMax}`,
34
+ };
35
+ }
36
+ return null;
37
+ }
38
+
39
+ // ---- text_surface ------------------------------------------------------
40
+ const textSurfaceDescriptor = {
41
+ kind: "text_surface",
42
+ refreshTargets: ["body"],
43
+ schemaBranch: {
44
+ title: "text_surface",
45
+ type: "object",
46
+ required: ["kind", "body"],
47
+ properties: {
48
+ kind: { const: "text_surface" },
49
+ title: { type: "string", maxLength: GLASSES_UI_LIMITS.titleMax },
50
+ body: { type: "string", maxLength: GLASSES_UI_LIMITS.bodyMax },
51
+ refresh: undefined, // filled by the tool when assembling the schema
52
+ },
53
+ },
54
+ validateSpec(obj) {
55
+ const titleErr = validateTitle(obj);
56
+ if (titleErr) return titleErr;
57
+ const body = obj.body;
58
+ if (typeof body !== "string") {
59
+ return { ok: false, code: "missing_field", message: "text_surface requires body (string)" };
60
+ }
61
+ if (body.length > GLASSES_UI_LIMITS.bodyMax) {
62
+ return {
63
+ ok: false,
64
+ code: "body_too_long",
65
+ message: `body is ${body.length} chars; max ${GLASSES_UI_LIMITS.bodyMax}`,
66
+ };
67
+ }
68
+ const spec = { kind: "text_surface", body };
69
+ if (typeof obj.title === "string") spec.title = obj.title;
70
+ return { ok: true, spec };
71
+ },
72
+ };
73
+
74
+ // ---- list_surface ------------------------------------------------------
75
+ const listSurfaceDescriptor = {
76
+ kind: "list_surface",
77
+ refreshTargets: ["items"],
78
+ schemaBranch: {
79
+ title: "list_surface",
80
+ type: "object",
81
+ required: ["kind", "items"],
82
+ properties: {
83
+ kind: { const: "list_surface" },
84
+ title: { type: "string", maxLength: GLASSES_UI_LIMITS.titleMax },
85
+ items: {
86
+ type: "array",
87
+ minItems: 1,
88
+ maxItems: GLASSES_UI_LIMITS.maxItems,
89
+ items: { type: "string", maxLength: GLASSES_UI_LIMITS.itemMax },
90
+ },
91
+ refresh: undefined,
92
+ },
93
+ },
94
+ validateSpec(obj) {
95
+ const titleErr = validateTitle(obj);
96
+ if (titleErr) return titleErr;
97
+ const items = obj.items;
98
+ if (!Array.isArray(items) || items.length === 0) {
99
+ return { ok: false, code: "missing_field", message: "list_surface requires items (non-empty array)" };
100
+ }
101
+ if (items.length > GLASSES_UI_LIMITS.maxItems) {
102
+ return {
103
+ ok: false,
104
+ code: "too_many_items",
105
+ message: `${items.length} items; max ${GLASSES_UI_LIMITS.maxItems}`,
106
+ };
107
+ }
108
+ for (let i = 0; i < items.length; i += 1) {
109
+ const item = items[i];
110
+ if (typeof item !== "string") {
111
+ return { ok: false, code: "item_too_long", message: `items[${i}] must be a string` };
112
+ }
113
+ if (item.length > GLASSES_UI_LIMITS.itemMax) {
114
+ return {
115
+ ok: false,
116
+ code: "item_too_long",
117
+ message: `items[${i}] is ${item.length} chars; max ${GLASSES_UI_LIMITS.itemMax}`,
118
+ };
119
+ }
120
+ }
121
+ const spec = { kind: "list_surface", items };
122
+ if (typeof obj.title === "string") spec.title = obj.title;
123
+ return { ok: true, spec };
124
+ },
125
+ };
126
+
127
+ // ---- list_with_details_surface ----------------------------------------
128
+ const listWithDetailsSurfaceDescriptor = {
129
+ kind: "list_with_details_surface",
130
+ refreshTargets: ["items"],
131
+ schemaBranch: {
132
+ title: "list_with_details_surface",
133
+ type: "object",
134
+ required: ["kind", "items"],
135
+ properties: {
136
+ kind: { const: "list_with_details_surface" },
137
+ title: { type: "string", maxLength: GLASSES_UI_LIMITS.titleMax },
138
+ items: {
139
+ type: "array",
140
+ minItems: 1,
141
+ maxItems: GLASSES_UI_LIMITS.maxItems,
142
+ items: {
143
+ type: "object",
144
+ required: ["label"],
145
+ properties: {
146
+ label: { type: "string", maxLength: GLASSES_UI_LIMITS.itemMax },
147
+ body: { type: "string", maxLength: GLASSES_UI_LIMITS.detailBodyMax },
148
+ },
149
+ },
150
+ },
151
+ refresh: undefined,
152
+ },
153
+ },
154
+ validateSpec(obj) {
155
+ const titleErr = validateTitle(obj);
156
+ if (titleErr) return titleErr;
157
+ // Empirically (2026-05-24 hardware logs), the agent often produces the
158
+ // parallel-array shape — items=[strings] plus a sibling `details` /
159
+ // `itemDetails` / `bodies` array — instead of the canonical [{label, body?}]
160
+ // objects. We accept the parallel-array shape and coerce it server-side so
161
+ // the wire format downstream stays canonical.
162
+ const rawItems = obj.items;
163
+ if (!Array.isArray(rawItems) || rawItems.length === 0) {
164
+ return {
165
+ ok: false,
166
+ code: "missing_field",
167
+ message: "list_with_details_surface requires items (non-empty array)",
168
+ };
169
+ }
170
+ if (rawItems.length > GLASSES_UI_LIMITS.maxItems) {
171
+ return {
172
+ ok: false,
173
+ code: "too_many_items",
174
+ message: `${rawItems.length} items; max ${GLASSES_UI_LIMITS.maxItems}`,
175
+ };
176
+ }
177
+ const parallelBodies =
178
+ Array.isArray(obj.details) ? obj.details
179
+ : Array.isArray(obj.itemDetails) ? obj.itemDetails
180
+ : Array.isArray(obj.bodies) ? obj.bodies
181
+ : null;
182
+ const items = rawItems.map((entry, i) => {
183
+ if (typeof entry === "string") {
184
+ const sibling = parallelBodies ? parallelBodies[i] : undefined;
185
+ // The sibling can be a plain string body, OR (some models do this) a
186
+ // {label, body} object that duplicates the label — accept either.
187
+ if (typeof sibling === "string") {
188
+ return { label: entry, body: sibling };
189
+ }
190
+ if (sibling && typeof sibling === "object" && typeof sibling.body === "string") {
191
+ return { label: entry, body: sibling.body };
192
+ }
193
+ // No matching parallel body — degrade gracefully to label-only.
194
+ return { label: entry };
195
+ }
196
+ return entry;
197
+ });
198
+ let totalBodyChars = 0;
199
+ const normalizedItems = [];
200
+ for (let i = 0; i < items.length; i += 1) {
201
+ const it = items[i];
202
+ if (!it || typeof it !== "object") {
203
+ return { ok: false, code: "missing_field", message: `items[${i}] must be an object {label, body?}` };
204
+ }
205
+ if (typeof it.label !== "string") {
206
+ return { ok: false, code: "missing_field", message: `items[${i}].label is required` };
207
+ }
208
+ if (it.label.length > GLASSES_UI_LIMITS.itemMax) {
209
+ return {
210
+ ok: false,
211
+ code: "item_too_long",
212
+ message: `items[${i}].label is ${it.label.length} chars; max ${GLASSES_UI_LIMITS.itemMax}`,
213
+ };
214
+ }
215
+ const normalized = { label: it.label };
216
+ if (it.body !== undefined) {
217
+ if (typeof it.body !== "string") {
218
+ return { ok: false, code: "detail_body_too_long", message: `items[${i}].body must be a string` };
219
+ }
220
+ if (it.body.length > GLASSES_UI_LIMITS.detailBodyMax) {
221
+ return {
222
+ ok: false,
223
+ code: "detail_body_too_long",
224
+ message: `items[${i}].body is ${it.body.length} chars; max ${GLASSES_UI_LIMITS.detailBodyMax}`,
225
+ };
226
+ }
227
+ totalBodyChars += it.body.length;
228
+ normalized.body = it.body;
229
+ }
230
+ normalizedItems.push(normalized);
231
+ }
232
+ if (totalBodyChars > GLASSES_UI_LIMITS.totalDetailPayloadMax) {
233
+ return {
234
+ ok: false,
235
+ code: "total_payload_too_large",
236
+ message: `bodies sum to ${totalBodyChars} chars; max ${GLASSES_UI_LIMITS.totalDetailPayloadMax}`,
237
+ };
238
+ }
239
+ const spec = { kind: "list_with_details_surface", items: normalizedItems };
240
+ if (typeof obj.title === "string") spec.title = obj.title;
241
+ return { ok: true, spec };
242
+ },
243
+ };
244
+
245
+ export const GLASSES_UI_KIND_DESCRIPTORS = [
246
+ textSurfaceDescriptor,
247
+ listSurfaceDescriptor,
248
+ listWithDetailsSurfaceDescriptor,
249
+ ];
250
+
251
+ export function getKindDescriptor(kind) {
252
+ return GLASSES_UI_KIND_DESCRIPTORS.find((d) => d.kind === kind);
253
+ }
254
+
255
+ export function listKindStrings() {
256
+ return GLASSES_UI_KIND_DESCRIPTORS.map((d) => d.kind);
257
+ }
258
+
259
+ export function buildOneOfBranches() {
260
+ return GLASSES_UI_KIND_DESCRIPTORS.map((d) => d.schemaBranch);
261
+ }
@@ -0,0 +1,21 @@
1
+ // Shared per-kind size caps for the glasses-UI surface tool. Extracted into its
2
+ // own leaf module so BOTH the tool (glasses-ui-tool.ts) and the kind-descriptor
3
+ // registry (glasses-ui-descriptors.ts) can import the same limits WITHOUT
4
+ // forming an import cycle between those two modules.
5
+ //
6
+ // Why a leaf module is required (not a cycle): the plugin's CJS emitter
7
+ // (scripts/build.mjs) converts `import {X} from "y"` into an in-place
8
+ // `const {X} = require("y.cjs")` and appends `module.exports = {...}` only at
9
+ // END of module execution (no live getters). Under those semantics a
10
+ // bidirectional descriptors<->tool require resolves to `{}` mid-cycle and
11
+ // crashes (GLASSES_UI_LIMITS undefined / listKindStrings not a function). Giving
12
+ // the limits their own dependency-free module makes the graph a DAG
13
+ // (tool -> {limits, descriptors}; descriptors -> limits), so no cycle exists.
14
+ export const GLASSES_UI_LIMITS = {
15
+ bodyMax: 1000,
16
+ itemMax: 64,
17
+ titleMax: 64,
18
+ maxItems: 20,
19
+ detailBodyMax: 200,
20
+ totalDetailPayloadMax: 6 * 1024,
21
+ };
@@ -0,0 +1,99 @@
1
+ // Per-surface trailing-edge paint-floor coalescer.
2
+ //
3
+ // Governs ALL plugin->glass sends (initial RebuildPageContainer render and
4
+ // every surface_update patch). Collapses bursts to last-write-wins per field
5
+ // and emits at most one frame per paintFloorMs (default 150ms, Spike D), with
6
+ // a leading-edge send + a trailing send carrying the final merged patch.
7
+ //
8
+ // There is NO glass-side paint-ack: the only backpressure signal is
9
+ // relay/BLE transport-side (see the isUnderBackpressure shed in Task 13).
10
+ // Local fake-list textContainerUpgrade scroll-swaps are client-side and never
11
+ // reach this coalescer.
12
+
13
+ export const DEFAULT_PAINT_FLOOR_MS = 150;
14
+
15
+ export function createPaintFloorCoalescer(deps) {
16
+ const paintFloorMs = Number.isFinite(deps.paintFloorMs) ? deps.paintFloorMs : DEFAULT_PAINT_FLOOR_MS;
17
+ const send = deps.send;
18
+ const nowMs = typeof deps.nowMs === "function" ? deps.nowMs : () => performance.now();
19
+ const setTimeoutFn = deps.setTimeoutFn || setTimeout;
20
+ const clearTimeoutFn = deps.clearTimeoutFn || clearTimeout;
21
+ const isUnderBackpressure =
22
+ typeof deps.isUnderBackpressure === "function" ? deps.isUnderBackpressure : () => false;
23
+
24
+ // surfaceId -> { sessionKey, lastSentAt, pendingPatch, timer }
25
+ const bySurface = new Map();
26
+
27
+ function isRenderSentinel(p) {
28
+ return !!(p && p.__render === true);
29
+ }
30
+
31
+ function mergePatch(base, incoming) {
32
+ // A render sentinel ({ __render, __depth, __spec }, Task 13) and a field
33
+ // patch ({ body }/{ items }/{ title }) must NEVER shallow-merge — a merged
34
+ // object would carry both __spec and a stray `body`, painting a malformed
35
+ // frame. They are different write kinds, so the LATER write supersedes the
36
+ // earlier one wholesale (a render replaces a queued field patch; a field
37
+ // patch after a queued render replaces that render). Same-kind writes
38
+ // merge last-write-wins per field as before.
39
+ if (isRenderSentinel(base) !== isRenderSentinel(incoming)) {
40
+ return incoming && typeof incoming === "object" ? { ...incoming } : {};
41
+ }
42
+ const merged = base ? { ...base } : {};
43
+ if (incoming && typeof incoming === "object") {
44
+ for (const k of Object.keys(incoming)) merged[k] = incoming[k];
45
+ }
46
+ return merged;
47
+ }
48
+
49
+ function flush(surfaceId) {
50
+ const st = bySurface.get(surfaceId);
51
+ if (!st || !st.pendingPatch) return;
52
+ st.timer = null;
53
+ if (isUnderBackpressure()) {
54
+ // Shed: do NOT send while the relay/BLE send buffer is over the high
55
+ // water mark. Retain pendingPatch (last-write-wins) and re-arm so the
56
+ // final merged value lands once pressure clears. No glass-side ack
57
+ // exists, so transport-side pressure is the only signal.
58
+ st.timer = setTimeoutFn(() => flush(surfaceId), Math.max(16, paintFloorMs));
59
+ return;
60
+ }
61
+ const patch = st.pendingPatch;
62
+ st.pendingPatch = null;
63
+ st.lastSentAt = nowMs();
64
+ send({ surfaceId, sessionKey: st.sessionKey, patch });
65
+ }
66
+
67
+ function enqueue(params) {
68
+ const { surfaceId, sessionKey, patch } = params;
69
+ let st = bySurface.get(surfaceId);
70
+ if (!st) {
71
+ st = { sessionKey, lastSentAt: -Infinity, pendingPatch: null, timer: null };
72
+ bySurface.set(surfaceId, st);
73
+ }
74
+ st.sessionKey = sessionKey;
75
+ const elapsed = nowMs() - st.lastSentAt;
76
+ if (elapsed >= paintFloorMs && !st.timer) {
77
+ // Leading edge: send immediately.
78
+ st.lastSentAt = nowMs();
79
+ send({ surfaceId, sessionKey, patch });
80
+ return;
81
+ }
82
+ // Within the floor: merge into the pending patch, arm a trailing flush.
83
+ st.pendingPatch = mergePatch(st.pendingPatch, patch);
84
+ if (!st.timer) {
85
+ const wait = Math.max(0, paintFloorMs - elapsed);
86
+ st.timer = setTimeoutFn(() => flush(surfaceId), wait);
87
+ }
88
+ }
89
+
90
+ function dispose(surfaceId) {
91
+ const st = bySurface.get(surfaceId);
92
+ if (st && st.timer) clearTimeoutFn(st.timer);
93
+ bySurface.delete(surfaceId);
94
+ }
95
+
96
+ return { enqueue, dispose, _bySurface: bySurface };
97
+ }
98
+
99
+ export default { createPaintFloorCoalescer, DEFAULT_PAINT_FLOOR_MS };