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
@@ -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,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
+ }