@goodandready/dsh-cron 0.3.3 → 0.3.4
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/analytics-digest.js +236 -0
- package/lib/api.js +41 -0
- package/lib/client.js +4718 -4515
- package/lib/prompt-evolver.js +148 -0
- package/lib/routes.js +79 -0
- package/lib/scheduler-execution.js +10 -0
- package/lib/task-create.js +3 -0
- package/lib/task-patch.js +9 -0
- package/lib/task-transfer.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Weekly AI Analytics & Cost Digest Engine (#330)
|
|
3
|
+
*
|
|
4
|
+
* Aggregates automation performance, success rates, token and USD spending,
|
|
5
|
+
* identifies top unstable and top expensive tasks, and formats delivery payloads.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export function buildAnalyticsDigest({ tasks = [], history = [], costLedger = [], periodDays = 7, now = Date.now() }) {
|
|
9
|
+
const windowDays = typeof periodDays === 'number' && periodDays > 0 ? periodDays : 7;
|
|
10
|
+
const windowStart = now - (windowDays * 86400000);
|
|
11
|
+
|
|
12
|
+
const taskList = Array.isArray(tasks) ? tasks : [];
|
|
13
|
+
const historyList = Array.isArray(history) ? history : [];
|
|
14
|
+
const costList = Array.isArray(costLedger) ? costLedger : [];
|
|
15
|
+
|
|
16
|
+
// Filter history runs in window
|
|
17
|
+
const runsInWindow = historyList.filter((r) => {
|
|
18
|
+
const t = r.startedAt || r.completedAt || r.timestamp;
|
|
19
|
+
return typeof t === 'number' && t >= windowStart;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
let totalRuns = 0;
|
|
23
|
+
let successfulRuns = 0;
|
|
24
|
+
let failedRuns = 0;
|
|
25
|
+
let totalTokens = 0;
|
|
26
|
+
let totalCostUsd = 0;
|
|
27
|
+
let healedCount = 0;
|
|
28
|
+
|
|
29
|
+
const taskStatsMap = new Map();
|
|
30
|
+
|
|
31
|
+
for (const task of taskList) {
|
|
32
|
+
taskStatsMap.set(task.id, {
|
|
33
|
+
id: task.id,
|
|
34
|
+
name: task.name || task.id,
|
|
35
|
+
runs: 0,
|
|
36
|
+
failures: 0,
|
|
37
|
+
tokens: 0,
|
|
38
|
+
costUsd: 0,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (runsInWindow.length > 0) {
|
|
43
|
+
totalRuns = runsInWindow.length;
|
|
44
|
+
for (const r of runsInWindow) {
|
|
45
|
+
const isFailed = r.status === 'failure' || r.status === 'failed' || Boolean(r.error);
|
|
46
|
+
if (isFailed) {
|
|
47
|
+
failedRuns += 1;
|
|
48
|
+
} else {
|
|
49
|
+
successfulRuns += 1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (r.healed || r.autoHealed || (typeof r.retryCount === 'number' && r.retryCount > 0 && !isFailed)) {
|
|
53
|
+
healedCount += 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const tokens = typeof r.tokens === 'number' ? r.tokens : (typeof r.tokensUsed === 'number' ? r.tokensUsed : 0);
|
|
57
|
+
totalTokens += tokens;
|
|
58
|
+
|
|
59
|
+
const cost = typeof r.costUsd === 'number' ? r.costUsd : 0;
|
|
60
|
+
totalCostUsd += cost;
|
|
61
|
+
|
|
62
|
+
if (r.taskId && taskStatsMap.has(r.taskId)) {
|
|
63
|
+
const ts = taskStatsMap.get(r.taskId);
|
|
64
|
+
ts.runs += 1;
|
|
65
|
+
if (isFailed) ts.failures += 1;
|
|
66
|
+
ts.tokens += tokens;
|
|
67
|
+
ts.costUsd += cost;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
// Fallback: aggregate from task summary counters
|
|
72
|
+
for (const task of taskList) {
|
|
73
|
+
const runs = typeof task.runs === 'number' ? task.runs : 0;
|
|
74
|
+
const failures = typeof task.failures === 'number' ? task.failures : 0;
|
|
75
|
+
const successes = Math.max(0, runs - failures);
|
|
76
|
+
const tokens = typeof task.tokensUsed === 'number' ? task.tokensUsed : 0;
|
|
77
|
+
const cost = typeof task.costUsd === 'number' ? task.costUsd : 0;
|
|
78
|
+
|
|
79
|
+
totalRuns += runs;
|
|
80
|
+
successfulRuns += successes;
|
|
81
|
+
failedRuns += failures;
|
|
82
|
+
totalTokens += tokens;
|
|
83
|
+
totalCostUsd += cost;
|
|
84
|
+
|
|
85
|
+
const ts = taskStatsMap.get(task.id);
|
|
86
|
+
if (ts) {
|
|
87
|
+
ts.runs = runs;
|
|
88
|
+
ts.failures = failures;
|
|
89
|
+
ts.tokens = tokens;
|
|
90
|
+
ts.costUsd = cost;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Calculate cost from ledger if available and higher
|
|
96
|
+
const ledgerCostInWindow = costList
|
|
97
|
+
.filter((c) => typeof c.timestamp === 'number' && c.timestamp >= windowStart)
|
|
98
|
+
.reduce((sum, c) => sum + (typeof c.costUsd === 'number' ? c.costUsd : 0), 0);
|
|
99
|
+
if (ledgerCostInWindow > totalCostUsd) {
|
|
100
|
+
totalCostUsd = ledgerCostInWindow;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const successRate = totalRuns > 0 ? Number(((successfulRuns / totalRuns) * 100).toFixed(1)) : 100.0;
|
|
104
|
+
|
|
105
|
+
// Identify top unstable tasks
|
|
106
|
+
const topUnstableTasks = Array.from(taskStatsMap.values())
|
|
107
|
+
.filter((ts) => ts.failures > 0)
|
|
108
|
+
.sort((a, b) => b.failures - a.failures || (b.runs - a.runs))
|
|
109
|
+
.slice(0, 3)
|
|
110
|
+
.map((ts) => ({
|
|
111
|
+
id: ts.id,
|
|
112
|
+
name: ts.name,
|
|
113
|
+
runs: ts.runs,
|
|
114
|
+
failures: ts.failures,
|
|
115
|
+
successRate: ts.runs > 0 ? Number((((ts.runs - ts.failures) / ts.runs) * 100).toFixed(1)) : 0.0,
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
// Identify top expensive tasks
|
|
119
|
+
const topExpensiveTasks = Array.from(taskStatsMap.values())
|
|
120
|
+
.filter((ts) => ts.tokens > 0 || ts.costUsd > 0)
|
|
121
|
+
.sort((a, b) => b.costUsd - a.costUsd || b.tokens - a.tokens)
|
|
122
|
+
.slice(0, 3)
|
|
123
|
+
.map((ts) => ({
|
|
124
|
+
id: ts.id,
|
|
125
|
+
name: ts.name,
|
|
126
|
+
tokens: ts.tokens,
|
|
127
|
+
costUsd: Number(ts.costUsd.toFixed(4)),
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
const activeTasksCount = taskList.filter((t) => t.status !== 'paused').length;
|
|
131
|
+
const pausedTasksCount = taskList.filter((t) => t.status === 'paused').length;
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
periodDays: windowDays,
|
|
135
|
+
windowStart,
|
|
136
|
+
windowEnd: now,
|
|
137
|
+
totalRuns,
|
|
138
|
+
successfulRuns,
|
|
139
|
+
failedRuns,
|
|
140
|
+
successRate,
|
|
141
|
+
totalTokens,
|
|
142
|
+
totalCostUsd: Number(totalCostUsd.toFixed(4)),
|
|
143
|
+
healedCount,
|
|
144
|
+
activeTasksCount,
|
|
145
|
+
pausedTasksCount,
|
|
146
|
+
totalTasksCount: taskList.length,
|
|
147
|
+
topUnstableTasks,
|
|
148
|
+
topExpensiveTasks,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function formatDigestMarkdown(digest) {
|
|
153
|
+
const {
|
|
154
|
+
periodDays,
|
|
155
|
+
totalRuns,
|
|
156
|
+
successfulRuns,
|
|
157
|
+
failedRuns,
|
|
158
|
+
successRate,
|
|
159
|
+
totalTokens,
|
|
160
|
+
totalCostUsd,
|
|
161
|
+
healedCount,
|
|
162
|
+
activeTasksCount,
|
|
163
|
+
pausedTasksCount,
|
|
164
|
+
topUnstableTasks,
|
|
165
|
+
topExpensiveTasks,
|
|
166
|
+
} = digest;
|
|
167
|
+
|
|
168
|
+
const lines = [
|
|
169
|
+
`📊 **DSH Cron Automation Digest (Last ${periodDays} Days)**`,
|
|
170
|
+
`• **Total Executions:** ${totalRuns} (✅ ${successfulRuns} succeeded, ❌ ${failedRuns} failed — **${successRate}%** SR)`,
|
|
171
|
+
`• **Token Consumption:** ${totalTokens.toLocaleString('en-US')} tokens (~$${totalCostUsd.toFixed(4)} USD)`,
|
|
172
|
+
`• **Task Fleet:** ${activeTasksCount} active, ${pausedTasksCount} paused`,
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
if (healedCount > 0) {
|
|
176
|
+
lines.push(`• **Self-Healed Runs:** 🔄 ${healedCount} recovered via retry/failure-inspector`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
lines.push('');
|
|
180
|
+
lines.push('⚠️ **Top Unstable Automations:**');
|
|
181
|
+
if (topUnstableTasks.length === 0) {
|
|
182
|
+
lines.push('*(All automations healthy — 0 failures recorded!)*');
|
|
183
|
+
} else {
|
|
184
|
+
topUnstableTasks.forEach((t, i) => {
|
|
185
|
+
lines.push(`${i + 1}. \`${t.name}\` — **${t.successRate}%** success (${t.failures} failed of ${t.runs} runs)`);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
lines.push('');
|
|
190
|
+
lines.push('💰 **Top Resource Consumers:**');
|
|
191
|
+
if (topExpensiveTasks.length === 0) {
|
|
192
|
+
lines.push('*(No significant token consumption recorded)*');
|
|
193
|
+
} else {
|
|
194
|
+
topExpensiveTasks.forEach((t, i) => {
|
|
195
|
+
lines.push(`${i + 1}. \`${t.name}\` — ${t.tokens.toLocaleString('en-US')} tokens ($${t.costUsd.toFixed(4)})`);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return lines.join('\n');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function formatDigestTelegram(digest) {
|
|
203
|
+
// Return clean plain Markdown compatible with standard bot message formatting
|
|
204
|
+
return formatDigestMarkdown(digest);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function sendAnalyticsDigest({ digest, channels, deliveryConfig }) {
|
|
208
|
+
if (!channels || !digest) {
|
|
209
|
+
throw new Error('channels and digest are required to send analytics digest');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const message = formatDigestMarkdown(digest);
|
|
213
|
+
const results = { telegram: null, webhook: null };
|
|
214
|
+
|
|
215
|
+
if (deliveryConfig?.telegram && typeof channels.sendTelegram === 'function') {
|
|
216
|
+
try {
|
|
217
|
+
results.telegram = await channels.sendTelegram(message, { parseMode: 'Markdown' });
|
|
218
|
+
} catch (err) {
|
|
219
|
+
results.telegram = { error: err.message };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (deliveryConfig?.webhookUrl && typeof channels.sendWebhook === 'function') {
|
|
224
|
+
try {
|
|
225
|
+
results.webhook = await channels.sendWebhook(deliveryConfig.webhookUrl, {
|
|
226
|
+
event: 'analytics.digest',
|
|
227
|
+
digest,
|
|
228
|
+
text: message,
|
|
229
|
+
});
|
|
230
|
+
} catch (err) {
|
|
231
|
+
results.webhook = { error: err.message };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return results;
|
|
236
|
+
}
|
package/lib/api.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rollbackPromptVersion, sanitizePromptHistory } from './prompt-evolver.js';
|
|
1
2
|
import { sanitizeAdaptiveConfig } from './adaptive.js';
|
|
2
3
|
import { logStreamer } from './log-streamer.js';
|
|
3
4
|
import { approvalManager } from './approval.js';
|
|
@@ -311,6 +312,20 @@ async function handleTaskItem({ store, scheduler, req, res, url, id, action, api
|
|
|
311
312
|
sendJson(res, 200, { ok: true, taskId: id, stats });
|
|
312
313
|
return;
|
|
313
314
|
}
|
|
315
|
+
if (action === 'prompt-history') {
|
|
316
|
+
const task = store.get(id);
|
|
317
|
+
if (!task) {
|
|
318
|
+
sendJson(res, 404, NOT_FOUND);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
sendJson(res, 200, {
|
|
322
|
+
ok: true,
|
|
323
|
+
taskId: id,
|
|
324
|
+
promptVersion: task.promptVersion || 1,
|
|
325
|
+
promptHistory: task.promptHistory || [],
|
|
326
|
+
});
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
314
329
|
if (action === 'history') {
|
|
315
330
|
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
316
331
|
sendJson(res, 200, { ok: true, history: store.getHistory(id, limit) });
|
|
@@ -350,6 +365,32 @@ async function handleTaskItem({ store, scheduler, req, res, url, id, action, api
|
|
|
350
365
|
|
|
351
366
|
/** Task actions that are plain POSTs. Returns true when one matched. */
|
|
352
367
|
async function handleItemPost({ store, scheduler, req, res, url, id, action }) {
|
|
368
|
+
if (action === 'prompt-rollback') {
|
|
369
|
+
const task = store.get(id);
|
|
370
|
+
if (!task) {
|
|
371
|
+
sendJson(res, 404, NOT_FOUND);
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
const body = await readBodyJson(req).catch(() => ({}));
|
|
375
|
+
const targetVersion = Number(body.version || url.searchParams.get('version'));
|
|
376
|
+
if (!targetVersion || targetVersion < 1) {
|
|
377
|
+
sendJson(res, 400, { ok: false, error: 'A valid target prompt version is required' });
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
try {
|
|
381
|
+
rollbackPromptVersion(task, targetVersion);
|
|
382
|
+
store.set(task);
|
|
383
|
+
sendJson(res, 200, {
|
|
384
|
+
ok: true,
|
|
385
|
+
task: redactTaskSecrets(task),
|
|
386
|
+
promptVersion: task.promptVersion,
|
|
387
|
+
promptHistory: task.promptHistory,
|
|
388
|
+
});
|
|
389
|
+
} catch (err) {
|
|
390
|
+
sendJson(res, 400, { ok: false, error: err.message });
|
|
391
|
+
}
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
353
394
|
if (action === 'retry-delivery') {
|
|
354
395
|
const task = store.get(id);
|
|
355
396
|
if (!task) {
|