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.
- package/README.md +18 -5
- package/dist/config/runtime-config.js +81 -3
- package/dist/domain/activity-status-adapter.js +138 -605
- package/dist/domain/activity-status-arbiter.js +109 -0
- package/dist/domain/activity-status-labels.js +906 -0
- package/dist/domain/code-span-regions.js +103 -0
- package/dist/domain/conversation-state.js +14 -1
- package/dist/domain/debug-store.js +38 -182
- package/dist/domain/glasses-ui-content-summary.js +62 -0
- package/dist/domain/glasses-ui-system-prompt.js +28 -0
- package/dist/domain/message-emoji-allowlist.js +16 -0
- package/dist/domain/message-emoji-filter.js +33 -55
- package/dist/domain/neural-emoji-reactor-system-prompt.js +43 -0
- package/dist/domain/neural-emoji-reactor-tag-config.js +56 -0
- package/dist/domain/neural-pace-modulator-system-prompt.js +32 -0
- package/dist/domain/neural-pace-modulator-tag-config.js +51 -0
- package/dist/domain/tagged-span-parser.js +121 -0
- package/dist/domain/tagged-span-strip.js +38 -0
- package/dist/even-ai/even-ai-endpoint.js +91 -0
- package/dist/even-ai/even-ai-run-waiter.js +14 -0
- package/dist/even-ai/even-ai-settings-store.js +14 -0
- package/dist/gateway/gateway-bridge.js +14 -2
- package/dist/gateway/gateway-timing-ledger.js +457 -0
- package/dist/gateway/openclaw-client.js +462 -38
- package/dist/index.js +28 -1
- package/dist/runtime/downstream-handler.js +754 -83
- package/dist/runtime/downstream-server.js +700 -534
- package/dist/runtime/ocuclaw-settings-store.js +74 -31
- package/dist/runtime/plugin-update-service.js +216 -0
- package/dist/runtime/protocol-adapter.js +9 -0
- package/dist/runtime/provider-usage-select.js +168 -0
- package/dist/runtime/relay-client-nudge-controller.js +553 -0
- package/dist/runtime/relay-core.js +1209 -204
- package/dist/runtime/relay-health-monitor.js +172 -0
- package/dist/runtime/relay-operation-registry.js +263 -0
- package/dist/runtime/relay-service.js +201 -1
- package/dist/runtime/relay-worker-approval-replay-cache.js +68 -0
- package/dist/runtime/relay-worker-entry.js +32 -0
- package/dist/runtime/relay-worker-health.js +272 -0
- package/dist/runtime/relay-worker-protocol.js +285 -0
- package/dist/runtime/relay-worker-queue.js +202 -0
- package/dist/runtime/relay-worker-supervisor.js +1081 -0
- package/dist/runtime/relay-worker-transport.js +1051 -0
- package/dist/runtime/session-context-service.js +189 -0
- package/dist/runtime/session-service.js +615 -24
- package/dist/runtime/upstream-runtime.js +1167 -60
- package/dist/tools/device-info-tool.js +242 -0
- package/dist/tools/glasses-ui-cron.js +427 -0
- package/dist/tools/glasses-ui-descriptors.js +261 -0
- package/dist/tools/glasses-ui-limits.js +21 -0
- package/dist/tools/glasses-ui-paint-floor.js +99 -0
- package/dist/tools/glasses-ui-recipes.js +746 -0
- package/dist/tools/glasses-ui-surfaces.js +278 -0
- package/dist/tools/glasses-ui-template.js +182 -0
- package/dist/tools/glasses-ui-tool.js +1147 -0
- package/dist/tools/session-title-tool.js +209 -0
- package/dist/version.js +2 -0
- package/openclaw.plugin.json +163 -15
- package/package.json +12 -4
|
@@ -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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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.
|
|
105
|
-
fs.
|
|
106
|
-
|
|
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,216 @@
|
|
|
1
|
+
import { PLUGIN_VERSION, REQUIRES_CLIENT_VERSION } from "../version.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plugin update service.
|
|
5
|
+
*
|
|
6
|
+
* Exposes the current plugin version (from build-time constant) and two
|
|
7
|
+
* single-flight executors that shell out to the openclaw CLI:
|
|
8
|
+
* - runPluginUpdate: `openclaw plugins update ocuclaw`
|
|
9
|
+
* - runGatewayRestart: `openclaw gateway restart` (detached)
|
|
10
|
+
*
|
|
11
|
+
* Injects child_process.spawn so tests can mock process behaviour.
|
|
12
|
+
*/
|
|
13
|
+
function createPluginUpdateService(deps) {
|
|
14
|
+
const spawn = deps.spawn;
|
|
15
|
+
const logger = deps.logger;
|
|
16
|
+
const nowMs = deps.nowMs;
|
|
17
|
+
const setTimeoutFn = deps.setTimeout;
|
|
18
|
+
const clearTimeoutFn = deps.clearTimeout;
|
|
19
|
+
const updateTimeoutMs = Number.isFinite(deps.updateTimeoutMs)
|
|
20
|
+
? Math.max(1, Math.floor(deps.updateTimeoutMs))
|
|
21
|
+
: 5 * 60 * 1000;
|
|
22
|
+
const killGraceMs = Number.isFinite(deps.killGraceMs)
|
|
23
|
+
? Math.max(1, Math.floor(deps.killGraceMs))
|
|
24
|
+
: 5 * 1000;
|
|
25
|
+
|
|
26
|
+
function getPluginVersion() {
|
|
27
|
+
return typeof PLUGIN_VERSION === "string" && PLUGIN_VERSION.length > 0
|
|
28
|
+
? PLUGIN_VERSION
|
|
29
|
+
: null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getRequiresClientVersion() {
|
|
33
|
+
return typeof REQUIRES_CLIENT_VERSION === "string" && REQUIRES_CLIENT_VERSION.length > 0
|
|
34
|
+
? REQUIRES_CLIENT_VERSION
|
|
35
|
+
: null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function captureStderrTail(stream, maxBytes) {
|
|
39
|
+
const chunks = [];
|
|
40
|
+
let totalBytes = 0;
|
|
41
|
+
let drained = false;
|
|
42
|
+
const drainCallbacks = [];
|
|
43
|
+
stream.on("data", (chunk) => {
|
|
44
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
45
|
+
chunks.push(buf);
|
|
46
|
+
totalBytes += buf.length;
|
|
47
|
+
while (totalBytes > maxBytes && chunks.length > 0) {
|
|
48
|
+
const head = chunks[0];
|
|
49
|
+
if (totalBytes - head.length >= maxBytes) {
|
|
50
|
+
// Dropping head still leaves more than maxBytes; drop it entirely.
|
|
51
|
+
chunks.shift();
|
|
52
|
+
totalBytes -= head.length;
|
|
53
|
+
} else {
|
|
54
|
+
// Partial drop: slice head so the total becomes exactly maxBytes.
|
|
55
|
+
const drop = totalBytes - maxBytes;
|
|
56
|
+
chunks[0] = head.subarray(drop);
|
|
57
|
+
totalBytes = maxBytes;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
const onDrained = () => {
|
|
63
|
+
if (drained) return;
|
|
64
|
+
drained = true;
|
|
65
|
+
for (const cb of drainCallbacks) cb();
|
|
66
|
+
};
|
|
67
|
+
stream.on("end", onDrained);
|
|
68
|
+
stream.on("close", onDrained);
|
|
69
|
+
stream.on("error", onDrained);
|
|
70
|
+
return {
|
|
71
|
+
readTail() {
|
|
72
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
73
|
+
},
|
|
74
|
+
whenDrained(cb) {
|
|
75
|
+
if (drained) { cb(); } else { drainCallbacks.push(cb); }
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let updateInFlight = false;
|
|
81
|
+
let restartStarted = false;
|
|
82
|
+
|
|
83
|
+
async function runPluginUpdate() {
|
|
84
|
+
if (updateInFlight) {
|
|
85
|
+
return { ok: false, reason: "in_progress" };
|
|
86
|
+
}
|
|
87
|
+
updateInFlight = true;
|
|
88
|
+
try {
|
|
89
|
+
return await doRunPluginUpdate();
|
|
90
|
+
} finally {
|
|
91
|
+
updateInFlight = false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function doRunPluginUpdate() {
|
|
96
|
+
return await new Promise((resolve) => {
|
|
97
|
+
const STDERR_MAX_BYTES = 8 * 1024;
|
|
98
|
+
let settled = false;
|
|
99
|
+
let timedOut = false;
|
|
100
|
+
let timeoutTimer = null;
|
|
101
|
+
let killTimer = null;
|
|
102
|
+
let safetyTimer = null;
|
|
103
|
+
const settle = (result) => {
|
|
104
|
+
if (settled) return;
|
|
105
|
+
settled = true;
|
|
106
|
+
if (timeoutTimer !== null) {
|
|
107
|
+
clearTimeoutFn(timeoutTimer);
|
|
108
|
+
timeoutTimer = null;
|
|
109
|
+
}
|
|
110
|
+
if (killTimer !== null) {
|
|
111
|
+
clearTimeoutFn(killTimer);
|
|
112
|
+
killTimer = null;
|
|
113
|
+
}
|
|
114
|
+
if (safetyTimer !== null) {
|
|
115
|
+
clearTimeoutFn(safetyTimer);
|
|
116
|
+
safetyTimer = null;
|
|
117
|
+
}
|
|
118
|
+
resolve(result);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
let child;
|
|
122
|
+
try {
|
|
123
|
+
child = spawn(
|
|
124
|
+
"openclaw",
|
|
125
|
+
["plugins", "update", "ocuclaw"],
|
|
126
|
+
{ env: process.env, stdio: ["ignore", "pipe", "pipe"] },
|
|
127
|
+
);
|
|
128
|
+
} catch (_err) {
|
|
129
|
+
settle({ ok: false, reason: "spawn_failed" });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const stderrBuf = child.stderr
|
|
134
|
+
? captureStderrTail(child.stderr, STDERR_MAX_BYTES)
|
|
135
|
+
: { readTail: () => "", whenDrained: (cb) => cb() };
|
|
136
|
+
|
|
137
|
+
child.on("error", (err) => {
|
|
138
|
+
const code = err && err.code;
|
|
139
|
+
if (code === "ENOENT") {
|
|
140
|
+
settle({ ok: false, reason: "cli_not_found" });
|
|
141
|
+
} else {
|
|
142
|
+
settle({ ok: false, reason: "spawn_failed" });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
child.on("exit", (code) => {
|
|
146
|
+
if (timedOut) {
|
|
147
|
+
settle({ ok: false, reason: "timeout" });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (code === 0) {
|
|
151
|
+
settle({ ok: true });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const exitCode = typeof code === "number" ? code : null;
|
|
155
|
+
stderrBuf.whenDrained(() => {
|
|
156
|
+
const tail = stderrBuf.readTail();
|
|
157
|
+
settle({
|
|
158
|
+
ok: false,
|
|
159
|
+
reason: "nonzero_exit",
|
|
160
|
+
exitCode,
|
|
161
|
+
stderrTail: tail.length > 0 ? tail : undefined,
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
timeoutTimer = setTimeoutFn(() => {
|
|
167
|
+
timeoutTimer = null;
|
|
168
|
+
timedOut = true;
|
|
169
|
+
try { child.kill("SIGTERM"); } catch (_) {}
|
|
170
|
+
killTimer = setTimeoutFn(() => {
|
|
171
|
+
killTimer = null;
|
|
172
|
+
try { child.kill("SIGKILL"); } catch (_) {}
|
|
173
|
+
}, killGraceMs);
|
|
174
|
+
// Safety net: if child never emits exit, settle after kill grace + buffer.
|
|
175
|
+
safetyTimer = setTimeoutFn(() => {
|
|
176
|
+
safetyTimer = null;
|
|
177
|
+
settle({ ok: false, reason: "timeout" });
|
|
178
|
+
}, killGraceMs + 50);
|
|
179
|
+
}, updateTimeoutMs);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function runGatewayRestart() {
|
|
184
|
+
if (restartStarted) {
|
|
185
|
+
return { ok: false, started: false, reason: "in_progress" };
|
|
186
|
+
}
|
|
187
|
+
let child;
|
|
188
|
+
try {
|
|
189
|
+
child = spawn(
|
|
190
|
+
"openclaw",
|
|
191
|
+
["gateway", "restart"],
|
|
192
|
+
{ detached: true, stdio: "ignore", env: process.env },
|
|
193
|
+
);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
const code = err && err.code;
|
|
196
|
+
if (code === "ENOENT") {
|
|
197
|
+
return { ok: false, started: false, reason: "cli_not_found" };
|
|
198
|
+
}
|
|
199
|
+
return { ok: false, started: false, reason: "spawn_failed" };
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
if (typeof child.unref === "function") child.unref();
|
|
203
|
+
} catch (_) {}
|
|
204
|
+
restartStarted = true;
|
|
205
|
+
return { ok: true, started: true };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
getPluginVersion,
|
|
210
|
+
getRequiresClientVersion,
|
|
211
|
+
runPluginUpdate,
|
|
212
|
+
runGatewayRestart,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export { createPluginUpdateService };
|
|
@@ -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
|
+
}
|