@chrysb/alphaclaw 0.6.2-beta.0 → 0.6.2-beta.2

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.
@@ -484,15 +484,18 @@
484
484
  }
485
485
 
486
486
  .cron-runs-trend-segment-ok {
487
- background: rgba(34, 197, 94, 0.75);
487
+ background: rgba(34, 255, 170, 0.82);
488
+ box-shadow: inset 0 0 0 1px rgba(34, 255, 170, 0.28), 0 0 8px rgba(34, 255, 170, 0.24);
488
489
  }
489
490
 
490
491
  .cron-runs-trend-segment-error {
491
- background: rgba(239, 68, 68, 0.78);
492
+ background: rgba(255, 74, 138, 0.84);
493
+ box-shadow: inset 0 0 0 1px rgba(255, 74, 138, 0.32), 0 0 8px rgba(255, 74, 138, 0.24);
492
494
  }
493
495
 
494
496
  .cron-runs-trend-segment-skipped {
495
- background: rgba(250, 204, 21, 0.7);
497
+ background: rgba(255, 214, 64, 0.82);
498
+ box-shadow: inset 0 0 0 1px rgba(255, 214, 64, 0.28), 0 0 8px rgba(255, 214, 64, 0.22);
496
499
  }
497
500
 
498
501
  .cron-runs-trend-label {
@@ -523,13 +526,16 @@
523
526
  }
524
527
 
525
528
  .cron-runs-trend-legend-dot.is-ok {
526
- background: rgba(34, 197, 94, 0.95);
529
+ background: rgba(34, 255, 170, 0.98);
530
+ box-shadow: 0 0 8px rgba(34, 255, 170, 0.6);
527
531
  }
528
532
 
529
533
  .cron-runs-trend-legend-dot.is-error {
530
- background: rgba(239, 68, 68, 0.95);
534
+ background: rgba(255, 74, 138, 0.98);
535
+ box-shadow: 0 0 8px rgba(255, 74, 138, 0.58);
531
536
  }
532
537
 
533
538
  .cron-runs-trend-legend-dot.is-skipped {
534
- background: rgba(250, 204, 21, 0.95);
539
+ background: rgba(255, 214, 64, 0.98);
540
+ box-shadow: 0 0 8px rgba(255, 214, 64, 0.5);
535
541
  }
@@ -7,7 +7,6 @@ import { formatCost, formatTokenCount } from "./cron-helpers.js";
7
7
  import { formatCronScheduleLabel } from "./cron-helpers.js";
8
8
  import { readUiSettings, updateUiSettings } from "../../lib/ui-settings.js";
9
9
  import {
10
- buildTokenTierByJobId,
11
10
  classifyRepeatingJobs,
12
11
  expandJobsToRollingSlots,
13
12
  getUpcomingSlots,
@@ -68,47 +67,195 @@ const renderLegend = () => html`
68
67
 
69
68
  const kNowRefreshMs = 60 * 1000;
70
69
  const kCalendarExpandedUiSettingKey = "cronCalendarExpanded";
70
+ const kRunWindow7dMs = 7 * 24 * 60 * 60 * 1000;
71
+ const kSlotRunToleranceMs = 45 * 60 * 1000;
72
+ const kUnknownTier = "unknown";
71
73
 
72
74
  const formatUpcomingTime = (timestampMs) => {
73
75
  const dateValue = new Date(timestampMs);
74
76
  return dateValue.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
75
77
  };
76
78
 
79
+ const getRunTotalTokens = (entry = {}) => {
80
+ const usage = entry?.usage || {};
81
+ const candidates = [
82
+ usage?.total_tokens,
83
+ usage?.totalTokens,
84
+ entry?.total_tokens,
85
+ entry?.totalTokens,
86
+ ];
87
+ for (const candidate of candidates) {
88
+ const numericValue = Number(candidate);
89
+ if (Number.isFinite(numericValue) && numericValue >= 0) return numericValue;
90
+ }
91
+ return 0;
92
+ };
93
+
94
+ const getRunEstimatedCost = (entry = {}) => {
95
+ const usage = entry?.usage || {};
96
+ const candidates = [
97
+ entry?.estimatedCost,
98
+ entry?.estimated_cost,
99
+ usage?.estimatedCost,
100
+ usage?.estimated_cost,
101
+ usage?.totalCost,
102
+ usage?.total_cost,
103
+ usage?.costUsd,
104
+ usage?.cost,
105
+ ];
106
+ for (const candidate of candidates) {
107
+ const numericValue = Number(candidate);
108
+ if (Number.isFinite(numericValue) && numericValue >= 0) return numericValue;
109
+ }
110
+ return null;
111
+ };
112
+
113
+ const buildRunSummaryByJobId = ({ runsByJobId = {}, nowMs = Date.now() } = {}) => {
114
+ const cutoffMs = Number(nowMs || Date.now()) - kRunWindow7dMs;
115
+ return Object.entries(runsByJobId || {}).reduce((accumulator, [jobId, runResult]) => {
116
+ const entries = Array.isArray(runResult?.entries) ? runResult.entries : [];
117
+ const recentEntries = entries.filter((entry) => {
118
+ const timestampMs = Number(entry?.ts || 0);
119
+ return Number.isFinite(timestampMs) && timestampMs >= cutoffMs && timestampMs <= nowMs;
120
+ });
121
+ const runCount = recentEntries.length;
122
+ const totalTokens = recentEntries.reduce(
123
+ (sum, entry) => sum + Number(getRunTotalTokens(entry) || 0),
124
+ 0,
125
+ );
126
+ const totalCost = recentEntries.reduce((sum, entry) => {
127
+ const cost = getRunEstimatedCost(entry);
128
+ return sum + Number(cost == null ? 0 : cost);
129
+ }, 0);
130
+ accumulator[String(jobId || "")] = {
131
+ runCount,
132
+ totalTokens,
133
+ totalCost,
134
+ avgTokensPerRun: runCount > 0 ? Math.round(totalTokens / runCount) : 0,
135
+ avgCostPerRun: runCount > 0 ? totalCost / runCount : 0,
136
+ };
137
+ return accumulator;
138
+ }, {});
139
+ };
140
+
141
+ const mapRunsToSlots = ({ slots = [], runsByJobId = {}, nowMs = Date.now() } = {}) => {
142
+ const runsBySlotKey = {};
143
+ const consumedRunTimestampsByJobId = {};
144
+ const runEntriesByJobId = Object.entries(runsByJobId || {}).reduce(
145
+ (accumulator, [jobId, runResult]) => {
146
+ const entries = Array.isArray(runResult?.entries) ? runResult.entries : [];
147
+ const normalizedEntries = entries
148
+ .map((entry) => ({ ...entry, ts: Number(entry?.ts || 0) }))
149
+ .filter((entry) => Number.isFinite(entry.ts) && entry.ts > 0)
150
+ .sort((left, right) => left.ts - right.ts);
151
+ accumulator[String(jobId || "")] = normalizedEntries;
152
+ return accumulator;
153
+ },
154
+ {},
155
+ );
156
+ slots.forEach((slot) => {
157
+ if (Number(slot?.scheduledAtMs || 0) > nowMs) return;
158
+ const jobId = String(slot?.jobId || "");
159
+ const runEntries = runEntriesByJobId[jobId] || [];
160
+ if (runEntries.length === 0) return;
161
+ const consumedSet = consumedRunTimestampsByJobId[jobId] || new Set();
162
+ consumedRunTimestampsByJobId[jobId] = consumedSet;
163
+ let nearestEntry = null;
164
+ let nearestDeltaMs = Number.MAX_SAFE_INTEGER;
165
+ runEntries.forEach((entry) => {
166
+ if (consumedSet.has(entry.ts)) return;
167
+ const deltaMs = Math.abs(entry.ts - Number(slot?.scheduledAtMs || 0));
168
+ if (deltaMs > kSlotRunToleranceMs) return;
169
+ if (deltaMs < nearestDeltaMs) {
170
+ nearestDeltaMs = deltaMs;
171
+ nearestEntry = entry;
172
+ }
173
+ });
174
+ if (!nearestEntry) return;
175
+ consumedSet.add(nearestEntry.ts);
176
+ runsBySlotKey[String(slot?.key || "")] = nearestEntry;
177
+ });
178
+ return runsBySlotKey;
179
+ };
180
+
181
+ const buildTierThresholds = (values = []) => {
182
+ const sortedValues = values
183
+ .map((value) => Number(value || 0))
184
+ .filter((value) => Number.isFinite(value) && value > 0)
185
+ .sort((left, right) => left - right);
186
+ if (sortedValues.length === 0) return null;
187
+ const percentileAt = (indexRatio = 0) => {
188
+ const index = Math.min(
189
+ sortedValues.length - 1,
190
+ Math.floor((sortedValues.length - 1) * indexRatio),
191
+ );
192
+ return sortedValues[Math.max(0, index)];
193
+ };
194
+ return {
195
+ q1: percentileAt(0.25),
196
+ q2: percentileAt(0.5),
197
+ p90: percentileAt(0.9),
198
+ };
199
+ };
200
+
201
+ const classifyTokenTier = ({
202
+ enabled = true,
203
+ tokenValue = 0,
204
+ thresholds = null,
205
+ } = {}) => {
206
+ if (!enabled) return "disabled";
207
+ const safeValue = Number(tokenValue || 0);
208
+ if (!Number.isFinite(safeValue) || safeValue <= 0 || !thresholds) return kUnknownTier;
209
+ if (safeValue <= thresholds.q1) return "low";
210
+ if (safeValue <= thresholds.q2) return "medium";
211
+ if (safeValue <= thresholds.p90) return "high";
212
+ return "very-high";
213
+ };
214
+
77
215
 
78
216
  const buildJobTooltipText = ({
79
217
  jobName = "",
80
218
  job = null,
81
- usage = {},
219
+ runSummary7d = {},
220
+ slotRun = null,
82
221
  latestRun = null,
83
222
  scheduledAtMs = 0,
84
223
  scheduledStatus = "",
224
+ nowMs = Date.now(),
85
225
  } = {}) => {
86
- const runCount = Number(usage?.runCount || 0);
87
- const totalTokens = Number(usage?.totalTokens || 0);
88
- const totalCost = Number(usage?.totalCost || 0);
89
- const avgTokensPerRun = runCount > 0
90
- ? Number(usage?.avgTokensPerRun || Math.round(totalTokens / runCount))
91
- : 0;
92
- const avgCostPerRun = runCount > 0 ? totalCost / runCount : 0;
226
+ const isPastSlot = Number(scheduledAtMs || 0) > 0 && Number(scheduledAtMs || 0) <= nowMs;
227
+ const runCount7d = Number(runSummary7d?.runCount || 0);
228
+ const avgTokensPerRun7d = Number(runSummary7d?.avgTokensPerRun || 0);
229
+ const avgCostPerRun7d = Number(runSummary7d?.avgCostPerRun || 0);
230
+ const slotRunTokens = getRunTotalTokens(slotRun || {});
231
+ const slotRunCost = getRunEstimatedCost(slotRun || {});
232
+ const slotRunStatus = String(slotRun?.status || "").trim().toLowerCase();
93
233
 
94
- const lines = [
95
- String(jobName || "Job"),
96
- `Avg tokens/run: ${runCount > 0 ? formatTokenCount(avgTokensPerRun) : "—"}`,
97
- `Avg cost/run: ${runCount > 0 ? formatCost(avgCostPerRun) : "—"}`,
98
- `Total cost: ${formatCost(totalCost)}`,
99
- ];
100
-
101
- if (runCount <= 0) {
102
- lines.push("Runs: none yet");
234
+ const lines = [String(jobName || "Job")];
235
+ if (isPastSlot) {
236
+ lines.push(`Run tokens: ${slotRun ? formatTokenCount(slotRunTokens) : "—"}`);
237
+ lines.push(`Run cost: ${slotRunCost == null ? "—" : formatCost(slotRunCost)}`);
238
+ lines.push(`Run status: ${slotRunStatus || scheduledStatus || "unknown"}`);
239
+ if (slotRun?.ts) {
240
+ lines.push(
241
+ `Run time: ${new Date(Number(slotRun.ts || 0)).toLocaleString()}`,
242
+ );
243
+ }
103
244
  } else {
104
- lines.push(`Runs: ${formatTokenCount(runCount)}`);
245
+ lines.push(
246
+ `Avg tokens/run (last 7d): ${runCount7d > 0 ? formatTokenCount(avgTokensPerRun7d) : "—"}`,
247
+ );
248
+ lines.push(
249
+ `Avg cost/run (last 7d): ${runCount7d > 0 ? formatCost(avgCostPerRun7d) : "—"}`,
250
+ );
251
+ lines.push(`Runs (last 7d): ${runCount7d > 0 ? formatTokenCount(runCount7d) : "none"}`);
105
252
  }
106
253
 
107
- if (latestRun?.status) {
254
+ if (!isPastSlot && latestRun?.status) {
108
255
  lines.push(
109
256
  `Latest run: ${latestRun.status} (${new Date(Number(latestRun.ts || 0)).toLocaleString()})`,
110
257
  );
111
- } else {
258
+ } else if (!isPastSlot) {
112
259
  lines.push("Latest run: none");
113
260
  }
114
261
  if (Number(job?.state?.runningAtMs || 0) > 0) {
@@ -125,7 +272,6 @@ const buildJobTooltipText = ({
125
272
 
126
273
  export const CronCalendar = ({
127
274
  jobs = [],
128
- usageByJobId = {},
129
275
  runsByJobId = {},
130
276
  onSelectJob = () => {},
131
277
  }) => {
@@ -163,10 +309,6 @@ export const CronCalendar = ({
163
309
  () => mapRunStatusesToSlots({ slots: timeline.slots, bulkRunsByJobId: runsByJobId, nowMs }),
164
310
  [timeline.slots, runsByJobId, nowMs],
165
311
  );
166
- const tokenTierByJobId = useMemo(
167
- () => buildTokenTierByJobId({ jobs, usageByJobId }),
168
- [jobs, usageByJobId],
169
- );
170
312
  const jobById = useMemo(
171
313
  () =>
172
314
  jobs.reduce((accumulator, job) => {
@@ -188,6 +330,63 @@ export const CronCalendar = ({
188
330
  }, {}),
189
331
  [runsByJobId],
190
332
  );
333
+ const runSummary7dByJobId = useMemo(
334
+ () => buildRunSummaryByJobId({ runsByJobId, nowMs }),
335
+ [runsByJobId, nowMs],
336
+ );
337
+ const runBySlotKey = useMemo(
338
+ () => mapRunsToSlots({ slots: timeline.slots, runsByJobId, nowMs }),
339
+ [timeline.slots, runsByJobId, nowMs],
340
+ );
341
+ const slotTierThresholds = useMemo(() => {
342
+ const values = [];
343
+ timeline.slots.forEach((slot) => {
344
+ const job = jobById[slot.jobId] || null;
345
+ if (!job || job.enabled === false) return;
346
+ const isPastSlot = Number(slot?.scheduledAtMs || 0) <= nowMs;
347
+ if (isPastSlot) {
348
+ const slotRunTokens = getRunTotalTokens(runBySlotKey[slot.key] || {});
349
+ if (slotRunTokens > 0) values.push(slotRunTokens);
350
+ return;
351
+ }
352
+ const projectedAvgTokens = Number(runSummary7dByJobId[slot.jobId]?.avgTokensPerRun || 0);
353
+ if (projectedAvgTokens > 0) values.push(projectedAvgTokens);
354
+ });
355
+ repeatingJobs.forEach((job) => {
356
+ const jobId = String(job?.id || "");
357
+ const projectedAvgTokens = Number(runSummary7dByJobId[jobId]?.avgTokensPerRun || 0);
358
+ if (projectedAvgTokens > 0) values.push(projectedAvgTokens);
359
+ });
360
+ return buildTierThresholds(values);
361
+ }, [jobById, nowMs, repeatingJobs, runBySlotKey, runSummary7dByJobId, timeline.slots]);
362
+ const getSlotTokenTier = useCallback((slot = null) => {
363
+ const jobId = String(slot?.jobId || "");
364
+ const job = jobById[jobId] || null;
365
+ const enabled = job?.enabled !== false;
366
+ const isPastSlot = Number(slot?.scheduledAtMs || 0) <= nowMs;
367
+ if (isPastSlot) {
368
+ const slotRunTokens = getRunTotalTokens(runBySlotKey[String(slot?.key || "")] || {});
369
+ return classifyTokenTier({
370
+ enabled,
371
+ tokenValue: slotRunTokens,
372
+ thresholds: slotTierThresholds,
373
+ });
374
+ }
375
+ const projectedAvgTokens = Number(runSummary7dByJobId[jobId]?.avgTokensPerRun || 0);
376
+ return classifyTokenTier({
377
+ enabled,
378
+ tokenValue: projectedAvgTokens,
379
+ thresholds: slotTierThresholds,
380
+ });
381
+ }, [jobById, nowMs, runBySlotKey, runSummary7dByJobId, slotTierThresholds]);
382
+ const getJobProjectedTier = useCallback((jobId = "") => {
383
+ const job = jobById[jobId] || null;
384
+ return classifyTokenTier({
385
+ enabled: job?.enabled !== false,
386
+ tokenValue: Number(runSummary7dByJobId[jobId]?.avgTokensPerRun || 0),
387
+ thresholds: slotTierThresholds,
388
+ });
389
+ }, [jobById, runSummary7dByJobId, slotTierThresholds]);
191
390
 
192
391
  const upcomingSlots = useMemo(
193
392
  () => getUpcomingSlots({ slots: timeline.slots, nowMs }),
@@ -223,13 +422,14 @@ export const CronCalendar = ({
223
422
  : html`
224
423
  <div class="cron-calendar-compact-strip">
225
424
  ${upcomingSlots.map((slot) => {
226
- const usage = usageByJobId[slot.jobId] || {};
227
425
  const tooltipText = buildJobTooltipText({
228
426
  jobName: slot.jobName,
229
427
  job: jobById[slot.jobId] || null,
230
- usage,
428
+ runSummary7d: runSummary7dByJobId[slot.jobId] || {},
429
+ slotRun: runBySlotKey[slot.key] || null,
231
430
  latestRun: latestRunByJobId[slot.jobId],
232
431
  scheduledAtMs: slot.scheduledAtMs,
432
+ nowMs,
233
433
  });
234
434
  return html`
235
435
  <${Tooltip}
@@ -309,15 +509,16 @@ export const CronCalendar = ({
309
509
  ${visibleSlots.map((slot) => {
310
510
  const status = statusBySlotKey[slot.key] || "";
311
511
  const isPast = slot.scheduledAtMs <= nowMs;
312
- const tokenTier = tokenTierByJobId[slot.jobId] || "unknown";
313
- const usage = usageByJobId[slot.jobId] || {};
512
+ const tokenTier = getSlotTokenTier(slot);
314
513
  const tooltipText = buildJobTooltipText({
315
514
  jobName: slot.jobName,
316
515
  job: jobById[slot.jobId] || null,
317
- usage,
516
+ runSummary7d: runSummary7dByJobId[slot.jobId] || {},
517
+ slotRun: runBySlotKey[slot.key] || null,
318
518
  latestRun: latestRunByJobId[slot.jobId],
319
519
  scheduledAtMs: slot.scheduledAtMs,
320
520
  scheduledStatus: status,
521
+ nowMs,
321
522
  });
322
523
  return html`
323
524
  <${Tooltip}
@@ -366,13 +567,16 @@ export const CronCalendar = ({
366
567
  <div class="cron-calendar-repeating-list">
367
568
  ${repeatingJobs.map((job) => {
368
569
  const jobId = String(job?.id || "");
369
- const usage = usageByJobId[jobId] || {};
370
- const avgTokensPerRun = Number(usage?.avgTokensPerRun || 0);
570
+ const avgTokensPerRun = Number(
571
+ runSummary7dByJobId[jobId]?.avgTokensPerRun || 0,
572
+ );
371
573
  const tooltipText = buildJobTooltipText({
372
574
  jobName: job.name || job.id,
373
575
  job,
374
- usage,
576
+ runSummary7d: runSummary7dByJobId[jobId] || {},
577
+ slotRun: null,
375
578
  latestRun: latestRunByJobId[jobId],
579
+ nowMs,
376
580
  });
377
581
  return html`
378
582
  <${Tooltip}
@@ -385,7 +589,7 @@ export const CronCalendar = ({
385
589
  class=${`cron-calendar-repeating-pill ${slotStateClassName({
386
590
  isPast: false,
387
591
  mappedStatus: "",
388
- tokenTier: tokenTierByJobId[jobId] || "unknown",
592
+ tokenTier: getJobProjectedTier(jobId),
389
593
  })}`}
390
594
  role="button"
391
595
  tabindex="0"
@@ -50,7 +50,14 @@ const getSessionUsageByKeyPattern = ({ keyPattern = "", sinceMs = 0 } = {}) => {
50
50
  COALESCE(model, '') AS model,
51
51
  COALESCE(provider, '') AS provider,
52
52
  COUNT(*) AS event_count,
53
- COUNT(DISTINCT COALESCE(NULLIF(session_key, ''), NULLIF(session_id, ''))) AS run_count,
53
+ COUNT(
54
+ DISTINCT COALESCE(
55
+ NULLIF(run_id, ''),
56
+ NULLIF(session_key, ''),
57
+ NULLIF(session_id, ''),
58
+ printf('event:%d', id)
59
+ )
60
+ ) AS run_count,
54
61
  SUM(COALESCE(input_tokens, 0)) AS input_tokens,
55
62
  SUM(COALESCE(output_tokens, 0)) AS output_tokens,
56
63
  SUM(COALESCE(cache_read_tokens, 0)) AS cache_read_tokens,
@@ -67,6 +74,28 @@ const getSessionUsageByKeyPattern = ({ keyPattern = "", sinceMs = 0 } = {}) => {
67
74
  $keyPattern: normalizedPattern,
68
75
  $sinceMs: Number.isFinite(Number(sinceMs)) ? Number(sinceMs) : 0,
69
76
  });
77
+ const totalsRow = database
78
+ .prepare(
79
+ `
80
+ SELECT
81
+ COUNT(*) AS event_count,
82
+ COUNT(
83
+ DISTINCT COALESCE(
84
+ NULLIF(run_id, ''),
85
+ NULLIF(session_key, ''),
86
+ NULLIF(session_id, ''),
87
+ printf('event:%d', id)
88
+ )
89
+ ) AS run_count
90
+ FROM usage_events
91
+ WHERE session_key LIKE $keyPattern
92
+ AND ($sinceMs <= 0 OR timestamp >= $sinceMs)
93
+ `,
94
+ )
95
+ .get({
96
+ $keyPattern: normalizedPattern,
97
+ $sinceMs: Number.isFinite(Number(sinceMs)) ? Number(sinceMs) : 0,
98
+ }) || {};
70
99
  const modelBreakdown = rows.map((row) => {
71
100
  const inputTokens = Number(row.input_tokens || 0);
72
101
  const outputTokens = Number(row.output_tokens || 0);
@@ -118,6 +147,8 @@ const getSessionUsageByKeyPattern = ({ keyPattern = "", sinceMs = 0 } = {}) => {
118
147
  runCount: 0,
119
148
  },
120
149
  );
150
+ totals.eventCount = Number(totalsRow.event_count || totals.eventCount || 0);
151
+ totals.runCount = Number(totalsRow.run_count || 0);
121
152
 
122
153
  return { totals, modelBreakdown };
123
154
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.6.2-beta.0",
3
+ "version": "0.6.2-beta.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },