@chrysb/alphaclaw 0.4.0 → 0.4.1-beta.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/lib/public/css/shell.css +21 -19
- package/lib/public/css/theme.css +17 -0
- package/lib/public/js/app.js +80 -5
- package/lib/public/js/components/file-viewer/editor-surface.js +5 -0
- package/lib/public/js/components/file-viewer/index.js +3 -0
- package/lib/public/js/components/file-viewer/markdown-split-view.js +2 -0
- package/lib/public/js/components/file-viewer/toolbar.js +13 -0
- package/lib/public/js/components/file-viewer/use-file-viewer.js +48 -13
- package/lib/public/js/components/google/account-row.js +34 -1
- package/lib/public/js/components/google/gmail-setup-wizard.js +450 -0
- package/lib/public/js/components/google/gmail-watch-toggle.js +81 -0
- package/lib/public/js/components/google/index.js +118 -4
- package/lib/public/js/components/google/use-gmail-watch.js +140 -0
- package/lib/public/js/components/scope-picker.js +1 -1
- package/lib/public/js/components/sidebar-git-panel.js +5 -6
- package/lib/public/js/components/sidebar.js +2 -0
- package/lib/public/js/components/toast.js +11 -7
- package/lib/public/js/components/usage-tab/constants.js +31 -0
- package/lib/public/js/components/usage-tab/formatters.js +24 -0
- package/lib/public/js/components/usage-tab/index.js +72 -0
- package/lib/public/js/components/usage-tab/overview-section.js +147 -0
- package/lib/public/js/components/usage-tab/sessions-section.js +175 -0
- package/lib/public/js/components/usage-tab/use-usage-tab.js +241 -0
- package/lib/public/js/components/webhooks.js +182 -129
- package/lib/public/js/lib/api.js +106 -1
- package/lib/public/js/lib/format.js +71 -0
- package/lib/server/constants.js +28 -0
- package/lib/server/gmail-push.js +109 -0
- package/lib/server/gmail-serve.js +254 -0
- package/lib/server/gmail-watch.js +725 -0
- package/lib/server/google-state.js +130 -0
- package/lib/server/helpers.js +5 -7
- package/lib/server/internal-files-migration.js +31 -3
- package/lib/server/routes/gmail.js +128 -0
- package/lib/server/routes/google.js +19 -0
- package/lib/server/routes/system.js +107 -0
- package/lib/server/routes/usage.js +29 -2
- package/lib/server/routes/webhooks.js +52 -17
- package/lib/server/usage-db.js +283 -15
- package/lib/server/watchdog.js +66 -0
- package/lib/server/webhook-middleware.js +99 -1
- package/lib/server/webhooks.js +214 -65
- package/lib/server.js +27 -0
- package/lib/setup/gitignore +3 -0
- package/lib/setup/hourly-git-sync.sh +1 -1
- package/package.json +1 -1
- package/lib/public/js/components/usage-tab.js +0 -531
|
@@ -6,9 +6,12 @@ const {
|
|
|
6
6
|
validateWebhookName,
|
|
7
7
|
} = require("../webhooks");
|
|
8
8
|
|
|
9
|
-
const isFiniteInteger = (value) =>
|
|
9
|
+
const isFiniteInteger = (value) =>
|
|
10
|
+
Number.isFinite(value) && Number.isInteger(value);
|
|
10
11
|
const parseBooleanFlag = (value) => {
|
|
11
|
-
const normalized = String(value == null ? "" : value)
|
|
12
|
+
const normalized = String(value == null ? "" : value)
|
|
13
|
+
.trim()
|
|
14
|
+
.toLowerCase();
|
|
12
15
|
return ["1", "true", "yes", "on"].includes(normalized);
|
|
13
16
|
};
|
|
14
17
|
|
|
@@ -40,20 +43,24 @@ const mergeWebhookAndSummary = ({ webhook, summary }) => {
|
|
|
40
43
|
};
|
|
41
44
|
|
|
42
45
|
const normalizeStatusFilter = (rawStatus) => {
|
|
43
|
-
const status = String(rawStatus || "all")
|
|
46
|
+
const status = String(rawStatus || "all")
|
|
47
|
+
.trim()
|
|
48
|
+
.toLowerCase();
|
|
44
49
|
if (["all", "success", "error"].includes(status)) return status;
|
|
45
50
|
return "all";
|
|
46
51
|
};
|
|
47
52
|
|
|
48
53
|
const buildWebhookUrls = ({ baseUrl, name }) => {
|
|
49
54
|
const fullUrl = `${baseUrl}/hooks/${name}`;
|
|
50
|
-
const token = String(
|
|
55
|
+
const token = String(
|
|
56
|
+
process.env.OPENCLAW_HOOKS_TOKEN || process.env.WEBHOOK_TOKEN || "",
|
|
57
|
+
).trim();
|
|
51
58
|
const queryStringUrl = token
|
|
52
59
|
? `${fullUrl}?token=${encodeURIComponent(token)}`
|
|
53
|
-
: `${fullUrl}?token=<
|
|
60
|
+
: `${fullUrl}?token=<OPENCLAW_HOOKS_TOKEN>`;
|
|
54
61
|
const authHeaderValue = token
|
|
55
62
|
? `Authorization: Bearer ${token}`
|
|
56
|
-
: "Authorization: Bearer <
|
|
63
|
+
: "Authorization: Bearer <OPENCLAW_HOOKS_TOKEN>";
|
|
57
64
|
return { fullUrl, queryStringUrl, authHeaderValue, hasRuntimeToken: !!token };
|
|
58
65
|
};
|
|
59
66
|
|
|
@@ -71,7 +78,8 @@ const registerWebhookRoutes = ({
|
|
|
71
78
|
getSnapshot: async () => ({ restartRequired: false }),
|
|
72
79
|
};
|
|
73
80
|
const resolvedRestartState = restartRequiredState || fallbackRestartState;
|
|
74
|
-
const { markRequired: markRestartRequired, getSnapshot: getRestartSnapshot } =
|
|
81
|
+
const { markRequired: markRestartRequired, getSnapshot: getRestartSnapshot } =
|
|
82
|
+
resolvedRestartState;
|
|
75
83
|
const runWebhookGitSync = async (action, name) => {
|
|
76
84
|
if (typeof shellCmd !== "function") return null;
|
|
77
85
|
const safeName = String(name || "").trim();
|
|
@@ -95,7 +103,8 @@ const registerWebhookRoutes = ({
|
|
|
95
103
|
mergeWebhookAndSummary({
|
|
96
104
|
webhook,
|
|
97
105
|
summary: summaryByHook.get(webhook.name),
|
|
98
|
-
})
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
99
108
|
res.json({ ok: true, webhooks });
|
|
100
109
|
} catch (err) {
|
|
101
110
|
res.status(500).json({ ok: false, error: err.message });
|
|
@@ -106,8 +115,11 @@ const registerWebhookRoutes = ({
|
|
|
106
115
|
try {
|
|
107
116
|
const name = validateWebhookName(req.params.name);
|
|
108
117
|
const detail = getWebhookDetail({ fs, constants, name });
|
|
109
|
-
if (!detail)
|
|
110
|
-
|
|
118
|
+
if (!detail)
|
|
119
|
+
return res.status(404).json({ ok: false, error: "Webhook not found" });
|
|
120
|
+
const summary = webhooksDb
|
|
121
|
+
.getHookSummaries()
|
|
122
|
+
.find((item) => item.hookName === name);
|
|
111
123
|
const merged = mergeWebhookAndSummary({ webhook: detail, summary });
|
|
112
124
|
const baseUrl = getBaseUrl(req);
|
|
113
125
|
const urls = buildWebhookUrls({ baseUrl, name });
|
|
@@ -151,7 +163,9 @@ const registerWebhookRoutes = ({
|
|
|
151
163
|
syncWarning,
|
|
152
164
|
});
|
|
153
165
|
} catch (err) {
|
|
154
|
-
const status = String(err.message || "").includes("already exists")
|
|
166
|
+
const status = String(err.message || "").includes("already exists")
|
|
167
|
+
? 409
|
|
168
|
+
: 400;
|
|
155
169
|
return res.status(status).json({ ok: false, error: err.message });
|
|
156
170
|
}
|
|
157
171
|
});
|
|
@@ -159,9 +173,23 @@ const registerWebhookRoutes = ({
|
|
|
159
173
|
app.delete("/api/webhooks/:name", async (req, res) => {
|
|
160
174
|
try {
|
|
161
175
|
const name = validateWebhookName(req.params.name);
|
|
162
|
-
const deleteTransformDir = parseBooleanFlag(
|
|
163
|
-
|
|
164
|
-
|
|
176
|
+
const deleteTransformDir = parseBooleanFlag(
|
|
177
|
+
req?.body?.deleteTransformDir,
|
|
178
|
+
);
|
|
179
|
+
const deletion = deleteWebhook({
|
|
180
|
+
fs,
|
|
181
|
+
constants,
|
|
182
|
+
name,
|
|
183
|
+
deleteTransformDir,
|
|
184
|
+
});
|
|
185
|
+
if (deletion?.managed) {
|
|
186
|
+
return res.status(409).json({
|
|
187
|
+
ok: false,
|
|
188
|
+
error: `Webhook "${name}" is managed by system setup and cannot be deleted`,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (!deletion?.removed)
|
|
192
|
+
return res.status(404).json({ ok: false, error: "Webhook not found" });
|
|
165
193
|
const deletedRequestCount = webhooksDb.deleteRequestsByHook(name);
|
|
166
194
|
const syncWarning = await runWebhookGitSync("delete", name);
|
|
167
195
|
markRestartRequired("webhooks");
|
|
@@ -184,9 +212,15 @@ const registerWebhookRoutes = ({
|
|
|
184
212
|
const limit = Number.parseInt(String(req.query.limit || 50), 10);
|
|
185
213
|
const offset = Number.parseInt(String(req.query.offset || 0), 10);
|
|
186
214
|
const status = normalizeStatusFilter(req.query.status);
|
|
187
|
-
const hasBadPaging =
|
|
215
|
+
const hasBadPaging =
|
|
216
|
+
!isFiniteInteger(limit) ||
|
|
217
|
+
limit <= 0 ||
|
|
218
|
+
!isFiniteInteger(offset) ||
|
|
219
|
+
offset < 0;
|
|
188
220
|
if (hasBadPaging) {
|
|
189
|
-
return res
|
|
221
|
+
return res
|
|
222
|
+
.status(400)
|
|
223
|
+
.json({ ok: false, error: "Invalid limit/offset" });
|
|
190
224
|
}
|
|
191
225
|
const requests = webhooksDb.getRequests(name, { limit, offset, status });
|
|
192
226
|
return res.json({ ok: true, requests });
|
|
@@ -203,7 +237,8 @@ const registerWebhookRoutes = ({
|
|
|
203
237
|
return res.status(400).json({ ok: false, error: "Invalid request id" });
|
|
204
238
|
}
|
|
205
239
|
const request = webhooksDb.getRequestById(name, requestId);
|
|
206
|
-
if (!request)
|
|
240
|
+
if (!request)
|
|
241
|
+
return res.status(404).json({ ok: false, error: "Request not found" });
|
|
207
242
|
return res.json({ ok: true, request });
|
|
208
243
|
} catch (err) {
|
|
209
244
|
return res.status(400).json({ ok: false, error: err.message });
|
package/lib/server/usage-db.js
CHANGED
|
@@ -7,7 +7,10 @@ const kMaxSessionLimit = 200;
|
|
|
7
7
|
const kDefaultDays = 30;
|
|
8
8
|
const kDefaultMaxPoints = 100;
|
|
9
9
|
const kMaxMaxPoints = 1000;
|
|
10
|
+
const kDayMs = 24 * 60 * 60 * 1000;
|
|
10
11
|
const kTokensPerMillion = 1_000_000;
|
|
12
|
+
const kUtcTimeZone = "UTC";
|
|
13
|
+
const kDayKeyFormatterCache = new Map();
|
|
11
14
|
const kGlobalModelPricing = {
|
|
12
15
|
"claude-opus-4-6": { input: 15.0, output: 75.0 },
|
|
13
16
|
"claude-sonnet-4-6": { input: 3.0, output: 15.0 },
|
|
@@ -31,6 +34,39 @@ const coerceInt = (value, fallbackValue = 0) => {
|
|
|
31
34
|
const clampInt = (value, minValue, maxValue, fallbackValue) =>
|
|
32
35
|
Math.min(maxValue, Math.max(minValue, coerceInt(value, fallbackValue)));
|
|
33
36
|
|
|
37
|
+
const normalizeTimeZone = (value) => {
|
|
38
|
+
const raw = String(value || "").trim();
|
|
39
|
+
if (!raw) return kUtcTimeZone;
|
|
40
|
+
try {
|
|
41
|
+
new Intl.DateTimeFormat("en-US", { timeZone: raw });
|
|
42
|
+
return raw;
|
|
43
|
+
} catch {
|
|
44
|
+
return kUtcTimeZone;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const getDayKeyFormatter = (timeZone) => {
|
|
49
|
+
if (kDayKeyFormatterCache.has(timeZone)) {
|
|
50
|
+
return kDayKeyFormatterCache.get(timeZone);
|
|
51
|
+
}
|
|
52
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
53
|
+
timeZone,
|
|
54
|
+
year: "numeric",
|
|
55
|
+
month: "2-digit",
|
|
56
|
+
day: "2-digit",
|
|
57
|
+
});
|
|
58
|
+
kDayKeyFormatterCache.set(timeZone, formatter);
|
|
59
|
+
return formatter;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const toTimeZoneDayKey = (timestampMs, timeZone) => {
|
|
63
|
+
const parts = getDayKeyFormatter(timeZone).formatToParts(new Date(timestampMs));
|
|
64
|
+
const year = parts.find((part) => part.type === "year")?.value || "0000";
|
|
65
|
+
const month = parts.find((part) => part.type === "month")?.value || "01";
|
|
66
|
+
const day = parts.find((part) => part.type === "day")?.value || "01";
|
|
67
|
+
return `${year}-${month}-${day}`;
|
|
68
|
+
};
|
|
69
|
+
|
|
34
70
|
const resolvePricing = (model) => {
|
|
35
71
|
const normalized = String(model || "").toLowerCase();
|
|
36
72
|
if (!normalized) return null;
|
|
@@ -174,11 +210,15 @@ const initUsageDb = ({ rootDir }) => {
|
|
|
174
210
|
|
|
175
211
|
const toDayKey = (timestampMs) => new Date(timestampMs).toISOString().slice(0, 10);
|
|
176
212
|
|
|
177
|
-
const getPeriodRange = (days) => {
|
|
213
|
+
const getPeriodRange = (days, timeZone = kUtcTimeZone) => {
|
|
178
214
|
const now = Date.now();
|
|
179
215
|
const safeDays = clampInt(days, 1, 3650, kDefaultDays);
|
|
180
|
-
const startMs = now - safeDays *
|
|
181
|
-
|
|
216
|
+
const startMs = now - safeDays * kDayMs;
|
|
217
|
+
const normalizedTimeZone = normalizeTimeZone(timeZone);
|
|
218
|
+
const startDay = normalizedTimeZone === kUtcTimeZone
|
|
219
|
+
? toDayKey(startMs)
|
|
220
|
+
: toTimeZoneDayKey(startMs, normalizedTimeZone);
|
|
221
|
+
return { now, safeDays, startDay, timeZone: normalizedTimeZone };
|
|
182
222
|
};
|
|
183
223
|
|
|
184
224
|
const appendCostToRows = (rows) =>
|
|
@@ -208,27 +248,253 @@ const appendCostToRows = (rows) =>
|
|
|
208
248
|
};
|
|
209
249
|
});
|
|
210
250
|
|
|
211
|
-
const
|
|
251
|
+
const parseAgentAndSourceFromSessionRef = (sessionRef) => {
|
|
252
|
+
const raw = String(sessionRef || "").trim();
|
|
253
|
+
if (!raw) {
|
|
254
|
+
return { agent: "unknown", source: "chat" };
|
|
255
|
+
}
|
|
256
|
+
const parts = raw.split(":");
|
|
257
|
+
const agent =
|
|
258
|
+
parts[0] === "agent" && String(parts[1] || "").trim()
|
|
259
|
+
? String(parts[1] || "").trim()
|
|
260
|
+
: "unknown";
|
|
261
|
+
const source = parts.includes("hook")
|
|
262
|
+
? "hooks"
|
|
263
|
+
: parts.includes("cron")
|
|
264
|
+
? "cron"
|
|
265
|
+
: "chat";
|
|
266
|
+
return { agent, source };
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const getAgentCostDistribution = ({
|
|
270
|
+
eventsRows = [],
|
|
271
|
+
startDay = "",
|
|
272
|
+
timeZone = kUtcTimeZone,
|
|
273
|
+
}) => {
|
|
274
|
+
const byAgent = new Map();
|
|
275
|
+
const ensureAgentBucket = (agent) => {
|
|
276
|
+
if (byAgent.has(agent)) return byAgent.get(agent);
|
|
277
|
+
const bucket = {
|
|
278
|
+
agent,
|
|
279
|
+
inputTokens: 0,
|
|
280
|
+
outputTokens: 0,
|
|
281
|
+
cacheReadTokens: 0,
|
|
282
|
+
cacheWriteTokens: 0,
|
|
283
|
+
totalTokens: 0,
|
|
284
|
+
totalCost: 0,
|
|
285
|
+
turnCount: 0,
|
|
286
|
+
sourceBreakdown: {
|
|
287
|
+
chat: {
|
|
288
|
+
source: "chat",
|
|
289
|
+
inputTokens: 0,
|
|
290
|
+
outputTokens: 0,
|
|
291
|
+
cacheReadTokens: 0,
|
|
292
|
+
cacheWriteTokens: 0,
|
|
293
|
+
totalTokens: 0,
|
|
294
|
+
totalCost: 0,
|
|
295
|
+
turnCount: 0,
|
|
296
|
+
},
|
|
297
|
+
hooks: {
|
|
298
|
+
source: "hooks",
|
|
299
|
+
inputTokens: 0,
|
|
300
|
+
outputTokens: 0,
|
|
301
|
+
cacheReadTokens: 0,
|
|
302
|
+
cacheWriteTokens: 0,
|
|
303
|
+
totalTokens: 0,
|
|
304
|
+
totalCost: 0,
|
|
305
|
+
turnCount: 0,
|
|
306
|
+
},
|
|
307
|
+
cron: {
|
|
308
|
+
source: "cron",
|
|
309
|
+
inputTokens: 0,
|
|
310
|
+
outputTokens: 0,
|
|
311
|
+
cacheReadTokens: 0,
|
|
312
|
+
cacheWriteTokens: 0,
|
|
313
|
+
totalTokens: 0,
|
|
314
|
+
totalCost: 0,
|
|
315
|
+
turnCount: 0,
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
byAgent.set(agent, bucket);
|
|
320
|
+
return bucket;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
for (const eventRow of eventsRows) {
|
|
324
|
+
const timestamp = coerceInt(eventRow.timestamp);
|
|
325
|
+
const dayKey = timeZone === kUtcTimeZone
|
|
326
|
+
? toDayKey(timestamp)
|
|
327
|
+
: toTimeZoneDayKey(timestamp, timeZone);
|
|
328
|
+
if (dayKey < startDay) continue;
|
|
329
|
+
|
|
330
|
+
const inputTokens = coerceInt(eventRow.input_tokens);
|
|
331
|
+
const outputTokens = coerceInt(eventRow.output_tokens);
|
|
332
|
+
const cacheReadTokens = coerceInt(eventRow.cache_read_tokens);
|
|
333
|
+
const cacheWriteTokens = coerceInt(eventRow.cache_write_tokens);
|
|
334
|
+
const totalTokens =
|
|
335
|
+
coerceInt(eventRow.total_tokens) ||
|
|
336
|
+
inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
|
|
337
|
+
const { totalCost } = deriveCostBreakdown({
|
|
338
|
+
inputTokens,
|
|
339
|
+
outputTokens,
|
|
340
|
+
cacheReadTokens,
|
|
341
|
+
cacheWriteTokens,
|
|
342
|
+
model: eventRow.model,
|
|
343
|
+
});
|
|
344
|
+
const sessionRef = String(eventRow.session_key || eventRow.session_id || "");
|
|
345
|
+
const { agent, source } = parseAgentAndSourceFromSessionRef(sessionRef);
|
|
346
|
+
const agentBucket = ensureAgentBucket(agent);
|
|
347
|
+
const sourceBucket = agentBucket.sourceBreakdown[source];
|
|
348
|
+
|
|
349
|
+
agentBucket.inputTokens += inputTokens;
|
|
350
|
+
agentBucket.outputTokens += outputTokens;
|
|
351
|
+
agentBucket.cacheReadTokens += cacheReadTokens;
|
|
352
|
+
agentBucket.cacheWriteTokens += cacheWriteTokens;
|
|
353
|
+
agentBucket.totalTokens += totalTokens;
|
|
354
|
+
agentBucket.totalCost += totalCost;
|
|
355
|
+
agentBucket.turnCount += 1;
|
|
356
|
+
|
|
357
|
+
sourceBucket.inputTokens += inputTokens;
|
|
358
|
+
sourceBucket.outputTokens += outputTokens;
|
|
359
|
+
sourceBucket.cacheReadTokens += cacheReadTokens;
|
|
360
|
+
sourceBucket.cacheWriteTokens += cacheWriteTokens;
|
|
361
|
+
sourceBucket.totalTokens += totalTokens;
|
|
362
|
+
sourceBucket.totalCost += totalCost;
|
|
363
|
+
sourceBucket.turnCount += 1;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const agents = Array.from(byAgent.values())
|
|
367
|
+
.map((bucket) => ({
|
|
368
|
+
agent: bucket.agent,
|
|
369
|
+
inputTokens: bucket.inputTokens,
|
|
370
|
+
outputTokens: bucket.outputTokens,
|
|
371
|
+
cacheReadTokens: bucket.cacheReadTokens,
|
|
372
|
+
cacheWriteTokens: bucket.cacheWriteTokens,
|
|
373
|
+
totalTokens: bucket.totalTokens,
|
|
374
|
+
totalCost: bucket.totalCost,
|
|
375
|
+
turnCount: bucket.turnCount,
|
|
376
|
+
sourceBreakdown: ["chat", "hooks", "cron"].map(
|
|
377
|
+
(source) => bucket.sourceBreakdown[source],
|
|
378
|
+
),
|
|
379
|
+
}))
|
|
380
|
+
.sort((a, b) => b.totalCost - a.totalCost);
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
agents,
|
|
384
|
+
totals: agents.reduce(
|
|
385
|
+
(acc, agentBucket) => {
|
|
386
|
+
acc.totalCost += Number(agentBucket.totalCost || 0);
|
|
387
|
+
acc.totalTokens += Number(agentBucket.totalTokens || 0);
|
|
388
|
+
acc.turnCount += Number(agentBucket.turnCount || 0);
|
|
389
|
+
return acc;
|
|
390
|
+
},
|
|
391
|
+
{ totalCost: 0, totalTokens: 0, turnCount: 0 },
|
|
392
|
+
),
|
|
393
|
+
};
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const getDailySummary = ({ days = kDefaultDays, timeZone = kUtcTimeZone } = {}) => {
|
|
212
397
|
const database = ensureDb();
|
|
213
|
-
const { safeDays, startDay } = getPeriodRange(
|
|
214
|
-
|
|
398
|
+
const { now, safeDays, startDay, timeZone: normalizedTimeZone } = getPeriodRange(
|
|
399
|
+
days,
|
|
400
|
+
timeZone,
|
|
401
|
+
);
|
|
402
|
+
let rows = [];
|
|
403
|
+
if (normalizedTimeZone === kUtcTimeZone) {
|
|
404
|
+
rows = database
|
|
405
|
+
.prepare(`
|
|
406
|
+
SELECT
|
|
407
|
+
date,
|
|
408
|
+
model,
|
|
409
|
+
provider,
|
|
410
|
+
input_tokens,
|
|
411
|
+
output_tokens,
|
|
412
|
+
cache_read_tokens,
|
|
413
|
+
cache_write_tokens,
|
|
414
|
+
total_tokens,
|
|
415
|
+
turn_count
|
|
416
|
+
FROM usage_daily
|
|
417
|
+
WHERE date >= $startDay
|
|
418
|
+
ORDER BY date ASC, total_tokens DESC
|
|
419
|
+
`)
|
|
420
|
+
.all({ $startDay: startDay });
|
|
421
|
+
} else {
|
|
422
|
+
const lookbackMs = now - (safeDays + 2) * kDayMs;
|
|
423
|
+
const eventRows = database
|
|
424
|
+
.prepare(`
|
|
425
|
+
SELECT
|
|
426
|
+
timestamp,
|
|
427
|
+
provider,
|
|
428
|
+
model,
|
|
429
|
+
input_tokens,
|
|
430
|
+
output_tokens,
|
|
431
|
+
cache_read_tokens,
|
|
432
|
+
cache_write_tokens,
|
|
433
|
+
total_tokens
|
|
434
|
+
FROM usage_events
|
|
435
|
+
WHERE timestamp >= $lookbackMs
|
|
436
|
+
ORDER BY timestamp ASC
|
|
437
|
+
`)
|
|
438
|
+
.all({ $lookbackMs: lookbackMs });
|
|
439
|
+
const byDateModel = new Map();
|
|
440
|
+
for (const eventRow of eventRows) {
|
|
441
|
+
const dayKey = toTimeZoneDayKey(coerceInt(eventRow.timestamp), normalizedTimeZone);
|
|
442
|
+
if (dayKey < startDay) continue;
|
|
443
|
+
const model = String(eventRow.model || "unknown");
|
|
444
|
+
const mapKey = `${dayKey}\u0000${model}`;
|
|
445
|
+
if (!byDateModel.has(mapKey)) {
|
|
446
|
+
byDateModel.set(mapKey, {
|
|
447
|
+
date: dayKey,
|
|
448
|
+
model,
|
|
449
|
+
provider: String(eventRow.provider || "unknown"),
|
|
450
|
+
input_tokens: 0,
|
|
451
|
+
output_tokens: 0,
|
|
452
|
+
cache_read_tokens: 0,
|
|
453
|
+
cache_write_tokens: 0,
|
|
454
|
+
total_tokens: 0,
|
|
455
|
+
turn_count: 0,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
const aggregate = byDateModel.get(mapKey);
|
|
459
|
+
aggregate.input_tokens += coerceInt(eventRow.input_tokens);
|
|
460
|
+
aggregate.output_tokens += coerceInt(eventRow.output_tokens);
|
|
461
|
+
aggregate.cache_read_tokens += coerceInt(eventRow.cache_read_tokens);
|
|
462
|
+
aggregate.cache_write_tokens += coerceInt(eventRow.cache_write_tokens);
|
|
463
|
+
aggregate.total_tokens += coerceInt(eventRow.total_tokens);
|
|
464
|
+
aggregate.turn_count += 1;
|
|
465
|
+
if (!aggregate.provider && eventRow.provider) {
|
|
466
|
+
aggregate.provider = String(eventRow.provider || "unknown");
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
rows = Array.from(byDateModel.values()).sort((a, b) => {
|
|
470
|
+
if (a.date === b.date) return b.total_tokens - a.total_tokens;
|
|
471
|
+
return a.date.localeCompare(b.date);
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
const enriched = appendCostToRows(rows);
|
|
475
|
+
const lookbackMs = now - (safeDays + 2) * kDayMs;
|
|
476
|
+
const eventsRows = database
|
|
215
477
|
.prepare(`
|
|
216
478
|
SELECT
|
|
217
|
-
|
|
479
|
+
timestamp,
|
|
480
|
+
session_id,
|
|
481
|
+
session_key,
|
|
218
482
|
model,
|
|
219
|
-
provider,
|
|
220
483
|
input_tokens,
|
|
221
484
|
output_tokens,
|
|
222
485
|
cache_read_tokens,
|
|
223
486
|
cache_write_tokens,
|
|
224
|
-
total_tokens
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
ORDER BY date ASC, total_tokens DESC
|
|
487
|
+
total_tokens
|
|
488
|
+
FROM usage_events
|
|
489
|
+
WHERE timestamp >= $lookbackMs
|
|
490
|
+
ORDER BY timestamp ASC
|
|
229
491
|
`)
|
|
230
|
-
.all({ $
|
|
231
|
-
const
|
|
492
|
+
.all({ $lookbackMs: lookbackMs });
|
|
493
|
+
const costByAgent = getAgentCostDistribution({
|
|
494
|
+
eventsRows,
|
|
495
|
+
startDay,
|
|
496
|
+
timeZone: normalizedTimeZone,
|
|
497
|
+
});
|
|
232
498
|
const byDate = new Map();
|
|
233
499
|
for (const row of enriched) {
|
|
234
500
|
if (!byDate.has(row.date)) byDate.set(row.date, []);
|
|
@@ -294,8 +560,10 @@ const getDailySummary = ({ days = kDefaultDays } = {}) => {
|
|
|
294
560
|
return {
|
|
295
561
|
updatedAt: Date.now(),
|
|
296
562
|
days: safeDays,
|
|
563
|
+
timeZone: normalizedTimeZone,
|
|
297
564
|
daily,
|
|
298
565
|
totals,
|
|
566
|
+
costByAgent,
|
|
299
567
|
};
|
|
300
568
|
};
|
|
301
569
|
|
package/lib/server/watchdog.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const {
|
|
2
2
|
kWatchdogCheckIntervalMs,
|
|
3
|
+
kWatchdogDegradedCheckIntervalMs,
|
|
4
|
+
kWatchdogStartupFailureThreshold,
|
|
3
5
|
kWatchdogMaxRepairAttempts,
|
|
4
6
|
kWatchdogCrashLoopWindowMs,
|
|
5
7
|
kWatchdogCrashLoopThreshold,
|
|
@@ -64,9 +66,34 @@ const createWatchdog = ({
|
|
|
64
66
|
expectedRestartInProgress: false,
|
|
65
67
|
expectedRestartUntilMs: 0,
|
|
66
68
|
pendingRecoveryNoticeSource: "",
|
|
69
|
+
startupConsecutiveHealthFailures: 0,
|
|
67
70
|
};
|
|
68
71
|
let healthTimer = null;
|
|
69
72
|
let bootstrapHealthTimer = null;
|
|
73
|
+
let degradedHealthTimer = null;
|
|
74
|
+
|
|
75
|
+
const clearDegradedHealthCheckTimer = () => {
|
|
76
|
+
if (!degradedHealthTimer) return;
|
|
77
|
+
clearTimeout(degradedHealthTimer);
|
|
78
|
+
degradedHealthTimer = null;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const scheduleDegradedHealthCheck = () => {
|
|
82
|
+
if (degradedHealthTimer) return;
|
|
83
|
+
if (state.health !== "degraded" || state.lifecycle !== "running") return;
|
|
84
|
+
degradedHealthTimer = setTimeout(async () => {
|
|
85
|
+
degradedHealthTimer = null;
|
|
86
|
+
if (state.health !== "degraded" || state.lifecycle !== "running") return;
|
|
87
|
+
await runHealthCheck({
|
|
88
|
+
source: "degraded_retry",
|
|
89
|
+
allowAutoRepair: false,
|
|
90
|
+
});
|
|
91
|
+
if (state.health === "degraded" && state.lifecycle === "running") {
|
|
92
|
+
scheduleDegradedHealthCheck();
|
|
93
|
+
}
|
|
94
|
+
}, kWatchdogDegradedCheckIntervalMs);
|
|
95
|
+
if (typeof degradedHealthTimer.unref === "function") degradedHealthTimer.unref();
|
|
96
|
+
};
|
|
70
97
|
|
|
71
98
|
const clearExpectedRestartWindow = () => {
|
|
72
99
|
state.expectedRestartInProgress = false;
|
|
@@ -337,6 +364,8 @@ const createWatchdog = ({
|
|
|
337
364
|
}
|
|
338
365
|
if (parsed.ok) {
|
|
339
366
|
const wasUnhealthy = state.health !== "healthy";
|
|
367
|
+
state.startupConsecutiveHealthFailures = 0;
|
|
368
|
+
clearDegradedHealthCheckTimer();
|
|
340
369
|
clearExpectedRestartWindow();
|
|
341
370
|
state.health = "healthy";
|
|
342
371
|
if (state.lifecycle !== "crash_loop") state.lifecycle = "running";
|
|
@@ -359,6 +388,8 @@ const createWatchdog = ({
|
|
|
359
388
|
return true;
|
|
360
389
|
}
|
|
361
390
|
if (restartWindowActive) {
|
|
391
|
+
state.startupConsecutiveHealthFailures = 0;
|
|
392
|
+
clearDegradedHealthCheckTimer();
|
|
362
393
|
logEvent(
|
|
363
394
|
"health_check",
|
|
364
395
|
source,
|
|
@@ -381,6 +412,8 @@ const createWatchdog = ({
|
|
|
381
412
|
state.lifecycle === "running" &&
|
|
382
413
|
!state.crashRecoveryActive;
|
|
383
414
|
if (withinStartupGrace) {
|
|
415
|
+
state.startupConsecutiveHealthFailures = 0;
|
|
416
|
+
clearDegradedHealthCheckTimer();
|
|
384
417
|
logEvent(
|
|
385
418
|
"health_check",
|
|
386
419
|
source,
|
|
@@ -397,7 +430,31 @@ const createWatchdog = ({
|
|
|
397
430
|
return false;
|
|
398
431
|
}
|
|
399
432
|
|
|
433
|
+
if (state.health === "unknown" && state.lifecycle === "running") {
|
|
434
|
+
state.startupConsecutiveHealthFailures += 1;
|
|
435
|
+
if (state.startupConsecutiveHealthFailures < kWatchdogStartupFailureThreshold) {
|
|
436
|
+
logEvent(
|
|
437
|
+
"health_check",
|
|
438
|
+
source,
|
|
439
|
+
"ok",
|
|
440
|
+
{
|
|
441
|
+
reason: parsed.reason,
|
|
442
|
+
result,
|
|
443
|
+
skipped: true,
|
|
444
|
+
startupFailureRetryActive: true,
|
|
445
|
+
startupConsecutiveFailures: state.startupConsecutiveHealthFailures,
|
|
446
|
+
startupFailureThreshold: kWatchdogStartupFailureThreshold,
|
|
447
|
+
},
|
|
448
|
+
correlationId,
|
|
449
|
+
);
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
} else {
|
|
453
|
+
state.startupConsecutiveHealthFailures = 0;
|
|
454
|
+
}
|
|
455
|
+
|
|
400
456
|
state.health = "degraded";
|
|
457
|
+
scheduleDegradedHealthCheck();
|
|
401
458
|
logEvent(
|
|
402
459
|
"health_check",
|
|
403
460
|
source,
|
|
@@ -435,6 +492,7 @@ const createWatchdog = ({
|
|
|
435
492
|
|
|
436
493
|
const onGatewayExit = ({ code, signal, expectedExit = false, stderrTail = [] } = {}) => {
|
|
437
494
|
const correlationId = createCorrelationId();
|
|
495
|
+
clearDegradedHealthCheckTimer();
|
|
438
496
|
if (expectedExit) {
|
|
439
497
|
state.lifecycle = "restarting";
|
|
440
498
|
state.health = "unknown";
|
|
@@ -504,8 +562,10 @@ const createWatchdog = ({
|
|
|
504
562
|
};
|
|
505
563
|
|
|
506
564
|
const onGatewayLaunch = ({ startedAt = Date.now(), pid = null } = {}) => {
|
|
565
|
+
clearDegradedHealthCheckTimer();
|
|
507
566
|
state.lifecycle = "running";
|
|
508
567
|
state.health = "unknown";
|
|
568
|
+
state.startupConsecutiveHealthFailures = 0;
|
|
509
569
|
state.crashRecoveryActive = false;
|
|
510
570
|
clearExpectedRestartWindow();
|
|
511
571
|
state.uptimeStartedAt = startedAt;
|
|
@@ -515,8 +575,10 @@ const createWatchdog = ({
|
|
|
515
575
|
};
|
|
516
576
|
|
|
517
577
|
const onExpectedRestart = () => {
|
|
578
|
+
clearDegradedHealthCheckTimer();
|
|
518
579
|
state.lifecycle = "restarting";
|
|
519
580
|
state.health = "unknown";
|
|
581
|
+
state.startupConsecutiveHealthFailures = 0;
|
|
520
582
|
state.crashRecoveryActive = false;
|
|
521
583
|
markExpectedRestartWindow();
|
|
522
584
|
startBootstrapHealthChecks();
|
|
@@ -533,14 +595,17 @@ const createWatchdog = ({
|
|
|
533
595
|
|
|
534
596
|
const start = () => {
|
|
535
597
|
if (healthTimer || bootstrapHealthTimer) return;
|
|
598
|
+
clearDegradedHealthCheckTimer();
|
|
536
599
|
state.lifecycle = "running";
|
|
537
600
|
state.health = "unknown";
|
|
601
|
+
state.startupConsecutiveHealthFailures = 0;
|
|
538
602
|
state.uptimeStartedAt = Date.now();
|
|
539
603
|
state.gatewayStartedAt = Date.now();
|
|
540
604
|
startBootstrapHealthChecks();
|
|
541
605
|
};
|
|
542
606
|
|
|
543
607
|
const stop = () => {
|
|
608
|
+
clearDegradedHealthCheckTimer();
|
|
544
609
|
if (bootstrapHealthTimer) {
|
|
545
610
|
clearTimeout(bootstrapHealthTimer);
|
|
546
611
|
bootstrapHealthTimer = null;
|
|
@@ -550,6 +615,7 @@ const createWatchdog = ({
|
|
|
550
615
|
healthTimer = null;
|
|
551
616
|
}
|
|
552
617
|
state.lifecycle = "stopped";
|
|
618
|
+
state.startupConsecutiveHealthFailures = 0;
|
|
553
619
|
};
|
|
554
620
|
|
|
555
621
|
const getStatus = () => {
|