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
@@ -3,6 +3,7 @@ import * as path from "node:path";
3
3
 
4
4
  const STORE_VERSION = 1;
5
5
  const STORE_FILENAME = "ocuclaw-settings.json";
6
+ const PERSIST_DEBOUNCE_MS = 250;
6
7
 
7
8
  function normalizeLogger(logger) {
8
9
  if (!logger || typeof logger !== "object") {
@@ -48,6 +49,10 @@ export function normalizeOcuClawDefaultThinking(value) {
48
49
  return "";
49
50
  }
50
51
 
52
+ export function normalizeOcuClawDefaultFastMode(value) {
53
+ return value === true;
54
+ }
55
+
51
56
  function isStoredSnapshotCanonical(value, snapshot) {
52
57
  if (!value || typeof value !== "object") {
53
58
  return false;
@@ -55,7 +60,8 @@ function isStoredSnapshotCanonical(value, snapshot) {
55
60
  return (
56
61
  normalizeTrimmedString(value.systemPrompt) === snapshot.systemPrompt &&
57
62
  normalizeTrimmedString(value.defaultModel) === snapshot.defaultModel &&
58
- normalizeOcuClawDefaultThinking(value.defaultThinking) === snapshot.defaultThinking
63
+ normalizeOcuClawDefaultThinking(value.defaultThinking) === snapshot.defaultThinking &&
64
+ normalizeOcuClawDefaultFastMode(value.defaultFastMode) === snapshot.defaultFastMode
59
65
  );
60
66
  }
61
67
 
@@ -64,6 +70,7 @@ export function normalizeOcuClawSettingsSnapshot(value = {}) {
64
70
  systemPrompt: normalizeOcuClawSystemPrompt(value.systemPrompt),
65
71
  defaultModel: normalizeOcuClawDefaultModel(value.defaultModel),
66
72
  defaultThinking: normalizeOcuClawDefaultThinking(value.defaultThinking),
73
+ defaultFastMode: normalizeOcuClawDefaultFastMode(value.defaultFastMode),
67
74
  };
68
75
  }
69
76
 
@@ -83,37 +90,26 @@ export function createOcuClawSettingsStore(opts = {}) {
83
90
  ? path.join(opts.stateDir.trim(), STORE_FILENAME)
84
91
  : null;
85
92
 
86
- function persistSnapshot(snapshot, reason) {
87
- if (!statePath) {
88
- emitDebug(
89
- "settings.loadsave",
90
- "ocuclaw_settings_persist_skipped",
91
- "debug",
92
- null,
93
- () => ({
94
- reason,
95
- systemPromptChars: snapshot.systemPrompt.length,
96
- defaultModel: snapshot.defaultModel,
97
- defaultThinking: snapshot.defaultThinking,
98
- }),
99
- );
100
- return;
101
- }
93
+ let pendingWrite = null;
94
+ let pendingWriteTimer = null;
95
+ let writeInFlight = false;
102
96
 
97
+ async function writeSnapshotToDisk(snapshot, reason) {
98
+ const payload =
99
+ JSON.stringify(
100
+ {
101
+ version: STORE_VERSION,
102
+ updatedAtMs: now(),
103
+ settings: snapshot,
104
+ },
105
+ null,
106
+ 2,
107
+ ) + "\n";
108
+ const tmpPath = `${statePath}.tmp`;
103
109
  try {
104
- fs.mkdirSync(path.dirname(statePath), { recursive: true });
105
- fs.writeFileSync(
106
- statePath,
107
- JSON.stringify(
108
- {
109
- version: STORE_VERSION,
110
- updatedAtMs: now(),
111
- settings: snapshot,
112
- },
113
- null,
114
- 2,
115
- ) + "\n",
116
- );
110
+ await fs.promises.mkdir(path.dirname(statePath), { recursive: true });
111
+ await fs.promises.writeFile(tmpPath, payload);
112
+ await fs.promises.rename(tmpPath, statePath);
117
113
  emitDebug(
118
114
  "settings.loadsave",
119
115
  "ocuclaw_settings_persisted",
@@ -125,6 +121,7 @@ export function createOcuClawSettingsStore(opts = {}) {
125
121
  systemPromptChars: snapshot.systemPrompt.length,
126
122
  defaultModel: snapshot.defaultModel,
127
123
  defaultThinking: snapshot.defaultThinking,
124
+ defaultFastMode: snapshot.defaultFastMode,
128
125
  }),
129
126
  );
130
127
  } catch (err) {
@@ -142,10 +139,52 @@ export function createOcuClawSettingsStore(opts = {}) {
142
139
  message: err && err.message ? err.message : String(err),
143
140
  }),
144
141
  );
145
- throw err;
146
142
  }
147
143
  }
148
144
 
145
+ function flushPendingWrite() {
146
+ if (writeInFlight || !pendingWrite) {
147
+ return;
148
+ }
149
+ const { snapshot, reason } = pendingWrite;
150
+ pendingWrite = null;
151
+ writeInFlight = true;
152
+ writeSnapshotToDisk(snapshot, reason).finally(() => {
153
+ writeInFlight = false;
154
+ if (pendingWrite) {
155
+ flushPendingWrite();
156
+ }
157
+ });
158
+ }
159
+
160
+ function persistSnapshot(snapshot, reason) {
161
+ if (!statePath) {
162
+ emitDebug(
163
+ "settings.loadsave",
164
+ "ocuclaw_settings_persist_skipped",
165
+ "debug",
166
+ null,
167
+ () => ({
168
+ reason,
169
+ systemPromptChars: snapshot.systemPrompt.length,
170
+ defaultModel: snapshot.defaultModel,
171
+ defaultThinking: snapshot.defaultThinking,
172
+ defaultFastMode: snapshot.defaultFastMode,
173
+ }),
174
+ );
175
+ return;
176
+ }
177
+
178
+ pendingWrite = { snapshot, reason };
179
+ if (pendingWriteTimer) {
180
+ clearTimeout(pendingWriteTimer);
181
+ }
182
+ pendingWriteTimer = setTimeout(() => {
183
+ pendingWriteTimer = null;
184
+ flushPendingWrite();
185
+ }, PERSIST_DEBOUNCE_MS);
186
+ }
187
+
149
188
  function loadInitialSnapshot() {
150
189
  if (!statePath || !fs.existsSync(statePath)) {
151
190
  persistSnapshot(defaults, "seed_defaults");
@@ -173,6 +212,7 @@ export function createOcuClawSettingsStore(opts = {}) {
173
212
  systemPromptChars: loaded.systemPrompt.length,
174
213
  defaultModel: loaded.defaultModel,
175
214
  defaultThinking: loaded.defaultThinking,
215
+ defaultFastMode: loaded.defaultFastMode,
176
216
  }),
177
217
  );
178
218
  if (
@@ -223,6 +263,9 @@ export function createOcuClawSettingsStore(opts = {}) {
223
263
  defaultThinking: hasOwn(patch, "defaultThinking")
224
264
  ? normalizeOcuClawDefaultThinking(patch.defaultThinking)
225
265
  : snapshot.defaultThinking,
266
+ defaultFastMode: hasOwn(patch, "defaultFastMode")
267
+ ? normalizeOcuClawDefaultFastMode(patch.defaultFastMode)
268
+ : snapshot.defaultFastMode,
226
269
  };
227
270
  snapshot = next;
228
271
  persistSnapshot(snapshot, "set_settings");
@@ -0,0 +1,23 @@
1
+ import { PLUGIN_VERSION, REQUIRES_CLIENT_VERSION } from "../version.js";
2
+
3
+ /**
4
+ * Plugin version provider. Exposes the build-time version constants used by the
5
+ * relay handshake. No process execution.
6
+ */
7
+ function createPluginVersionService() {
8
+ function getPluginVersion() {
9
+ return typeof PLUGIN_VERSION === "string" && PLUGIN_VERSION.length > 0
10
+ ? PLUGIN_VERSION
11
+ : null;
12
+ }
13
+
14
+ function getRequiresClientVersion() {
15
+ return typeof REQUIRES_CLIENT_VERSION === "string" && REQUIRES_CLIENT_VERSION.length > 0
16
+ ? REQUIRES_CLIENT_VERSION
17
+ : null;
18
+ }
19
+
20
+ return { getPluginVersion, getRequiresClientVersion };
21
+ }
22
+
23
+ export { createPluginVersionService };
@@ -4,8 +4,11 @@ const V1_TO_INTERNAL = {
4
4
  switchSession: "ocuclaw.session.switch",
5
5
  newChat: "ocuclaw.session.reset",
6
6
  getSessions: "ocuclaw.session.list",
7
+ deleteSessions: "ocuclaw.session.delete",
8
+ setSessionPinned: "ocuclaw.session.pinned.set",
7
9
  getStatus: "ocuclaw.runtime.status.get",
8
10
  getModelsCatalog: "ocuclaw.model.catalog.get",
11
+ getProviderUsageSnapshot: "ocuclaw.provider.usage.get",
9
12
  getSkills: "ocuclaw.skills.catalog.get",
10
13
  getSonioxModels: "ocuclaw.voice.soniox.models.get",
11
14
  getSessionModelConfig: "ocuclaw.session.config.get",
@@ -22,6 +25,7 @@ const V1_TO_INTERNAL = {
22
25
  "debug-set": "ocuclaw.debug.config.set",
23
26
  "debug-dump": "ocuclaw.debug.events.query",
24
27
  resume: "ocuclaw.sync.resume",
28
+ compactSession: "ocuclaw.session.compact",
25
29
  };
26
30
 
27
31
  const RESULT_TO_V1 = {
@@ -36,15 +40,18 @@ const RESULT_TO_V1 = {
36
40
  "ocuclaw.settings.snapshot": "ocuClawSettings",
37
41
  "ocuclaw.settings.set.ack": "ocuClawSettingsAck",
38
42
  "ocuclaw.model.catalog.snapshot": "modelsCatalog",
43
+ "ocuclaw.provider.usage.snapshot": "providerUsageSnapshot",
39
44
  "ocuclaw.skills.catalog.snapshot": "skillsCatalog",
40
45
  "ocuclaw.voice.soniox.models.snapshot": "sonioxModels",
41
46
  "ocuclaw.approval.resolve.ack": "approvalResponseAck",
47
+ "ocuclaw.session.compact.ack": "compactSessionAck",
42
48
  };
43
49
 
44
50
  const EVENT_TO_V1 = {
45
51
  "ocuclaw.view.pages.snapshot": "pages",
46
52
  "ocuclaw.runtime.status": "status",
47
53
  "ocuclaw.activity.update": "activity",
54
+ "ocuclaw.typing.update": "typing",
48
55
  "ocuclaw.message.stream.delta": "streaming",
49
56
  "ocuclaw.session.switch.applied": "sessionSwitched",
50
57
  "ocuclaw.sync.resume.ack": "resume-ack",
@@ -52,6 +59,8 @@ const EVENT_TO_V1 = {
52
59
  "ocuclaw.approval.resolved": "approvalResolved",
53
60
  "ocuclaw.remote.control": "remote-control",
54
61
  "ocuclaw.protocol.tap.frame": "protocol",
62
+ "ocuclaw.provider.usage.snapshot": "providerUsageSnapshot",
63
+ "ocuclaw.session.context.snapshot": "sessionContextSnapshot",
55
64
  };
56
65
 
57
66
  const INTERNAL_TO_V1 = Object.fromEntries(
@@ -0,0 +1,168 @@
1
+ function normalizeWindowKey(label, index) {
2
+ const normalized = typeof label === "string" ? label.trim().toLowerCase() : "";
3
+
4
+ if (normalized === "week" || normalized === "weekly") {
5
+ return { key: "week", sortOrder: 20 };
6
+ }
7
+
8
+ if (/^5\s*(h|hr|hrs|hour|hours)?$/.test(normalized)) {
9
+ return { key: "5h", sortOrder: 10 };
10
+ }
11
+
12
+ const key = normalized.replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
13
+ return {
14
+ key: key || `window_${index}`,
15
+ sortOrder: 100 + index,
16
+ };
17
+ }
18
+
19
+ function toFiniteNumber(value, fallback) {
20
+ if (value === null || value === undefined || value === "") {
21
+ return fallback;
22
+ }
23
+ const number = Number(value);
24
+ return Number.isFinite(number) ? number : fallback;
25
+ }
26
+
27
+ function normalizeWindow(window, index) {
28
+ const normalizedKey = normalizeWindowKey(window && window.label, index);
29
+ const label =
30
+ typeof window?.label === "string" && window.label.trim()
31
+ ? window.label.trim()
32
+ : normalizedKey.key;
33
+
34
+ return {
35
+ key: normalizedKey.key,
36
+ label,
37
+ usedPercent: toFiniteNumber(window && window.usedPercent, 0),
38
+ resetAtMs: toFiniteNumber(window && window.resetAt, null),
39
+ sortOrder: normalizedKey.sortOrder,
40
+ };
41
+ }
42
+
43
+ function isStrongerWindow(candidate, current) {
44
+ if (candidate.usedPercent !== current.usedPercent) {
45
+ return candidate.usedPercent > current.usedPercent;
46
+ }
47
+ return candidate.sortOrder > current.sortOrder;
48
+ }
49
+
50
+ export function selectLimitingWindow(windows) {
51
+ if (!Array.isArray(windows) || windows.length === 0) {
52
+ return null;
53
+ }
54
+
55
+ return windows.slice().sort((left, right) => {
56
+ const leftExhausted = left.usedPercent >= 100;
57
+ const rightExhausted = right.usedPercent >= 100;
58
+
59
+ if (leftExhausted !== rightExhausted) {
60
+ return leftExhausted ? -1 : 1;
61
+ }
62
+
63
+ if (left.usedPercent !== right.usedPercent) {
64
+ return right.usedPercent - left.usedPercent;
65
+ }
66
+
67
+ return right.sortOrder - left.sortOrder;
68
+ })[0];
69
+ }
70
+
71
+ export function selectProviderUsageSnapshot(summary, opts = {}) {
72
+ const providers = Array.isArray(summary && summary.providers) ? summary.providers : [];
73
+ const activeProvider =
74
+ typeof opts.provider === "string" ? opts.provider.trim().toLowerCase() : "";
75
+
76
+ if (!activeProvider) {
77
+ return null;
78
+ }
79
+
80
+ const namedEntries = providers.filter(
81
+ (entry) => typeof entry?.provider === "string",
82
+ );
83
+
84
+ let match = namedEntries.find(
85
+ (entry) => entry.provider.trim().toLowerCase() === activeProvider,
86
+ );
87
+
88
+ // Family fallback: the session model config reports the base provider id
89
+ // (e.g. "openai") while the usage summary keys the same usage by its
90
+ // sub-provider source (e.g. "openai-codex"). When there is no exact match
91
+ // but exactly ONE summary entry belongs to the active provider's family
92
+ // ("${activeProvider}-…"), resolve it. Restricted to a single family member
93
+ // to avoid ambiguous attribution when multiple sub-providers exist.
94
+ if (!match) {
95
+ const familyMatches = namedEntries.filter((entry) =>
96
+ entry.provider.trim().toLowerCase().startsWith(`${activeProvider}-`),
97
+ );
98
+ if (familyMatches.length === 1) {
99
+ match = familyMatches[0];
100
+ }
101
+ }
102
+
103
+ if (!match) {
104
+ return null;
105
+ }
106
+
107
+ const windows = (Array.isArray(match.windows) ? match.windows : []).map(normalizeWindow);
108
+ const limitingWindow = selectLimitingWindow(windows);
109
+ const dedupedWindows = [];
110
+ const keyToIndex = new Map();
111
+
112
+ for (const window of windows) {
113
+ if (!keyToIndex.has(window.key)) {
114
+ keyToIndex.set(window.key, dedupedWindows.length);
115
+ dedupedWindows.push(window);
116
+ continue;
117
+ }
118
+
119
+ const existingIndex = keyToIndex.get(window.key);
120
+ const existingWindow = dedupedWindows[existingIndex];
121
+ if (isStrongerWindow(window, existingWindow)) {
122
+ dedupedWindows[existingIndex] = window;
123
+ }
124
+ }
125
+
126
+ const provider = typeof match.provider === "string" ? match.provider.trim() : match.provider;
127
+
128
+ return {
129
+ sessionKey: typeof opts.sessionKey === "string" ? opts.sessionKey : null,
130
+ provider,
131
+ displayName:
132
+ typeof match.displayName === "string" && match.displayName.trim()
133
+ ? match.displayName.trim()
134
+ : provider,
135
+ fetchedAtMs: toFiniteNumber(summary && summary.updatedAt, null),
136
+ stale: opts.stale === true,
137
+ limitingWindowKey: limitingWindow ? limitingWindow.key : null,
138
+ windows: dedupedWindows,
139
+ };
140
+ }
141
+
142
+ export function buildRateLimitInfoFromSnapshot(snapshot) {
143
+ if (!snapshot || snapshot.stale === true || !snapshot.limitingWindowKey) {
144
+ return null;
145
+ }
146
+ if (snapshot.poolStatus === "ready") {
147
+ return null;
148
+ }
149
+
150
+ const limitingWindow = Array.isArray(snapshot.windows)
151
+ ? snapshot.windows.find((window) => window.key === snapshot.limitingWindowKey) || null
152
+ : null;
153
+
154
+ if (!limitingWindow) {
155
+ return null;
156
+ }
157
+
158
+ return {
159
+ sessionKey: snapshot.sessionKey || null,
160
+ provider: snapshot.provider || null,
161
+ windowKey: limitingWindow.key,
162
+ windowLabel: limitingWindow.label,
163
+ usedPercent: limitingWindow.usedPercent,
164
+ resetAtMs: limitingWindow.resetAtMs,
165
+ fetchedAtMs: snapshot.fetchedAtMs,
166
+ stale: false,
167
+ };
168
+ }