@goodandready/dsh-cron 0.3.2 → 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/README.md +23 -0
- package/lib/adaptive.js +151 -0
- package/lib/analytics-digest.js +236 -0
- package/lib/api.js +49 -0
- package/lib/client.js +4721 -4465
- package/lib/log-streamer.js +153 -0
- package/lib/prompt-evolver.js +148 -0
- package/lib/routes.js +79 -0
- package/lib/runner-external.js +10 -0
- package/lib/runner.js +18 -1
- package/lib/scheduler-execution.js +36 -0
- package/lib/scheduler.js +38 -0
- package/lib/task-create.js +5 -0
- package/lib/task-patch.js +13 -0
- package/lib/task-transfer.js +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -565,6 +565,7 @@ All endpoints are served by the DSH web server under `/dsh-cron/`. Endpoints are
|
|
|
565
565
|
| `POST` | `/dsh-cron/tasks` | Create or update a task (`id` present → update). Requires `title`, `schedule`, `prompt` |
|
|
566
566
|
| `GET` | `/dsh-cron/tasks/:id/history` | Run history, `?limit=20` |
|
|
567
567
|
| `POST` | `/dsh-cron/tasks/:id/run` | Trigger an immediate manual run |
|
|
568
|
+
| `GET` | `/dsh-cron/tasks/:id/stream` | Real-time Server-Sent Events (SSE) execution log streaming with ring buffer (#336) |
|
|
568
569
|
| `POST` | `/dsh-cron/tasks/:id/pause` | Pause the schedule |
|
|
569
570
|
| `POST` | `/dsh-cron/tasks/:id/resume` | Resume the schedule |
|
|
570
571
|
| `POST` | `/dsh-cron/tasks/:id/toggle` | Toggle active/paused |
|
|
@@ -590,6 +591,28 @@ All endpoints are served by the DSH web server under `/dsh-cron/`. Endpoints are
|
|
|
590
591
|
|
|
591
592
|
---
|
|
592
593
|
|
|
594
|
+
### ⚡ Adaptive Polling & Exponential Backoff (#329)
|
|
595
|
+
|
|
596
|
+
Save LLM tokens and API quotas on watcher tasks by dynamically slowing down execution when no changes occur:
|
|
597
|
+
|
|
598
|
+
```json
|
|
599
|
+
"adaptive": {
|
|
600
|
+
"enabled": true,
|
|
601
|
+
"minInterval": "5m",
|
|
602
|
+
"maxInterval": "2h",
|
|
603
|
+
"backoffFactor": 1.5,
|
|
604
|
+
"idleRunsThreshold": 3
|
|
605
|
+
}
|
|
606
|
+
```
|
|
607
|
+
- Multiplies interval by `backoffFactor` up to `maxInterval` after `idleRunsThreshold` silent or unchanged runs.
|
|
608
|
+
- Instantly resets to `minInterval` as soon as real changes or non-silent output are detected.
|
|
609
|
+
|
|
610
|
+
### 📺 Real-Time Streaming Logs via SSE (#336)
|
|
611
|
+
|
|
612
|
+
Connect to `GET /dsh-cron/tasks/:id/stream` using Server-Sent Events (SSE) to receive live stdout/stderr chunks and execution completion events with an automatic 500-line ring buffer.
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
593
616
|
## 🧪 Testing & Preflight Verification
|
|
594
617
|
|
|
595
618
|
Run the automated test suite covering schedule parsing, the scheduler engine, atomic storage, HTTP helpers, notifications and tool contracts:
|
package/lib/adaptive.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adaptive Polling & Exponential Backoff (#329)
|
|
3
|
+
*
|
|
4
|
+
* Automatically scales execution intervals up when no changes / idle runs are detected
|
|
5
|
+
* (e.g. silent skips, unchanged outputs) and instantly resets back to minInterval
|
|
6
|
+
* when new data is returned.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { logger } from './logger.js';
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_ADAPTIVE_CONFIG = {
|
|
12
|
+
enabled: false,
|
|
13
|
+
minInterval: '5m',
|
|
14
|
+
maxInterval: '2h',
|
|
15
|
+
backoffFactor: 1.5,
|
|
16
|
+
idleRunsThreshold: 3,
|
|
17
|
+
detectUnchangedOutput: true,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parse human interval string (e.g. "30s", "5m", "2h", "1d") or number into milliseconds.
|
|
22
|
+
*/
|
|
23
|
+
export function parseIntervalMs(val, defaultMs = 300000) {
|
|
24
|
+
if (typeof val === 'number' && !isNaN(val) && val > 0) {
|
|
25
|
+
return Math.round(val);
|
|
26
|
+
}
|
|
27
|
+
if (!val || typeof val !== 'string') return defaultMs;
|
|
28
|
+
|
|
29
|
+
const match = val.trim().match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)?$/i);
|
|
30
|
+
if (!match) return defaultMs;
|
|
31
|
+
|
|
32
|
+
const num = parseFloat(match[1]);
|
|
33
|
+
const unit = (match[2] || 'm').toLowerCase();
|
|
34
|
+
|
|
35
|
+
switch (unit) {
|
|
36
|
+
case 'ms': return Math.round(num);
|
|
37
|
+
case 's': return Math.round(num * 1000);
|
|
38
|
+
case 'm': return Math.round(num * 60 * 1000);
|
|
39
|
+
case 'h': return Math.round(num * 3600 * 1000);
|
|
40
|
+
case 'd': return Math.round(num * 86400 * 1000);
|
|
41
|
+
default: return defaultMs;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Format milliseconds into human readable interval (e.g. "5m", "1h 30m", "45s").
|
|
47
|
+
*/
|
|
48
|
+
export function formatInterval(ms) {
|
|
49
|
+
if (!ms || typeof ms !== 'number' || ms <= 0) return '0s';
|
|
50
|
+
const sec = Math.round(ms / 1000);
|
|
51
|
+
if (sec < 60) return `${sec}s`;
|
|
52
|
+
const min = Math.floor(sec / 60);
|
|
53
|
+
const remSec = sec % 60;
|
|
54
|
+
if (min < 60) return remSec > 0 ? `${min}m ${remSec}s` : `${min}m`;
|
|
55
|
+
const hrs = Math.floor(min / 60);
|
|
56
|
+
const remMin = min % 60;
|
|
57
|
+
return remMin > 0 ? `${hrs}h ${remMin}m` : `${hrs}h`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Sanitize and normalize adaptive configuration from payload.
|
|
62
|
+
*/
|
|
63
|
+
export function sanitizeAdaptiveConfig(adaptive) {
|
|
64
|
+
if (!adaptive || typeof adaptive !== 'object') {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const enabled = Boolean(adaptive.enabled);
|
|
68
|
+
const minInterval = typeof adaptive.minInterval === 'string' ? adaptive.minInterval.trim() : (adaptive.minInterval ? String(adaptive.minInterval) : '5m');
|
|
69
|
+
const maxInterval = typeof adaptive.maxInterval === 'string' ? adaptive.maxInterval.trim() : (adaptive.maxInterval ? String(adaptive.maxInterval) : '2h');
|
|
70
|
+
const backoffFactor = (typeof adaptive.backoffFactor === 'number' && adaptive.backoffFactor >= 1) ? adaptive.backoffFactor : 1.5;
|
|
71
|
+
const idleRunsThreshold = (typeof adaptive.idleRunsThreshold === 'number' && adaptive.idleRunsThreshold >= 1) ? Math.floor(adaptive.idleRunsThreshold) : 3;
|
|
72
|
+
const detectUnchangedOutput = adaptive.detectUnchangedOutput !== false;
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
enabled,
|
|
76
|
+
minInterval,
|
|
77
|
+
maxInterval,
|
|
78
|
+
backoffFactor,
|
|
79
|
+
idleRunsThreshold,
|
|
80
|
+
detectUnchangedOutput,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Evaluate if a completed run constitutes an "idle / no-change" execution.
|
|
86
|
+
*/
|
|
87
|
+
export function evaluateAdaptiveRun(task, outcome, runInfo, lastRun) {
|
|
88
|
+
if (outcome?.status === 'silentSkip' || runInfo?.silentSkip === true) {
|
|
89
|
+
return { isIdle: true, reason: 'silent_rule' };
|
|
90
|
+
}
|
|
91
|
+
if (runInfo?.validation && runInfo.validation.noChange === true) {
|
|
92
|
+
return { isIdle: true, reason: 'validator_no_change' };
|
|
93
|
+
}
|
|
94
|
+
if (task.adaptive?.detectUnchangedOutput !== false && lastRun && outcome?.status === 'success') {
|
|
95
|
+
const currentOut = String(outcome.output || '').trim();
|
|
96
|
+
const lastOut = String(lastRun.output || '').trim();
|
|
97
|
+
if (currentOut && lastOut && currentOut === lastOut) {
|
|
98
|
+
return { isIdle: true, reason: 'unchanged_output' };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { isIdle: false, reason: 'active_output' };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Calculate the next adaptive state and schedule interval.
|
|
106
|
+
*/
|
|
107
|
+
export function calculateNextAdaptiveState(task, isIdle, now = Date.now()) {
|
|
108
|
+
const config = sanitizeAdaptiveConfig(task.adaptive) || DEFAULT_ADAPTIVE_CONFIG;
|
|
109
|
+
const minMs = parseIntervalMs(config.minInterval, 300000);
|
|
110
|
+
const maxMs = parseIntervalMs(config.maxInterval, 7200000);
|
|
111
|
+
const factor = config.backoffFactor || 1.5;
|
|
112
|
+
const threshold = config.idleRunsThreshold || 3;
|
|
113
|
+
|
|
114
|
+
const prevState = task.adaptiveState || {
|
|
115
|
+
idleCount: 0,
|
|
116
|
+
currentIntervalMs: minMs,
|
|
117
|
+
effectiveIntervalStr: formatInterval(minMs),
|
|
118
|
+
active: false,
|
|
119
|
+
lastAdjustedAt: now,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
if (!isIdle) {
|
|
123
|
+
// Reset back to base minInterval immediately upon real changes
|
|
124
|
+
return {
|
|
125
|
+
idleCount: 0,
|
|
126
|
+
currentIntervalMs: minMs,
|
|
127
|
+
effectiveIntervalStr: formatInterval(minMs),
|
|
128
|
+
active: false,
|
|
129
|
+
lastAdjustedAt: now,
|
|
130
|
+
backedOff: false,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const idleCount = (prevState.idleCount || 0) + 1;
|
|
135
|
+
let currentIntervalMs = prevState.currentIntervalMs || minMs;
|
|
136
|
+
let backedOff = false;
|
|
137
|
+
|
|
138
|
+
if (idleCount >= threshold) {
|
|
139
|
+
backedOff = true;
|
|
140
|
+
currentIntervalMs = Math.min(Math.round(currentIntervalMs * factor), maxMs);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
idleCount,
|
|
145
|
+
currentIntervalMs,
|
|
146
|
+
effectiveIntervalStr: formatInterval(currentIntervalMs),
|
|
147
|
+
active: backedOff,
|
|
148
|
+
lastAdjustedAt: now,
|
|
149
|
+
backedOff,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -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,6 @@
|
|
|
1
|
+
import { rollbackPromptVersion, sanitizePromptHistory } from './prompt-evolver.js';
|
|
2
|
+
import { sanitizeAdaptiveConfig } from './adaptive.js';
|
|
3
|
+
import { logStreamer } from './log-streamer.js';
|
|
1
4
|
import { approvalManager } from './approval.js';
|
|
2
5
|
import { sanitizeValidators } from './validators.js';
|
|
3
6
|
/**
|
|
@@ -289,6 +292,10 @@ async function handleTaskItem({ store, scheduler, req, res, url, id, action, api
|
|
|
289
292
|
}
|
|
290
293
|
|
|
291
294
|
if (req.method === 'GET') {
|
|
295
|
+
if (action === 'stream') {
|
|
296
|
+
logStreamer.handleSseRequest(id, req, res);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
292
299
|
if (rejectCrossOrigin(req, res, { apiToken })) return;
|
|
293
300
|
if (action === 'archive') {
|
|
294
301
|
const limit = parseInt(url.searchParams.get('limit') || '50', 10);
|
|
@@ -305,6 +312,20 @@ async function handleTaskItem({ store, scheduler, req, res, url, id, action, api
|
|
|
305
312
|
sendJson(res, 200, { ok: true, taskId: id, stats });
|
|
306
313
|
return;
|
|
307
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
|
+
}
|
|
308
329
|
if (action === 'history') {
|
|
309
330
|
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
310
331
|
sendJson(res, 200, { ok: true, history: store.getHistory(id, limit) });
|
|
@@ -344,6 +365,32 @@ async function handleTaskItem({ store, scheduler, req, res, url, id, action, api
|
|
|
344
365
|
|
|
345
366
|
/** Task actions that are plain POSTs. Returns true when one matched. */
|
|
346
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
|
+
}
|
|
347
394
|
if (action === 'retry-delivery') {
|
|
348
395
|
const task = store.get(id);
|
|
349
396
|
if (!task) {
|
|
@@ -596,6 +643,8 @@ function mergeExecutionFields(current, body, parsed) {
|
|
|
596
643
|
concurrencyGroup: body.concurrencyGroup !== undefined ? String(body.concurrencyGroup).trim() : (current ? (current.concurrencyGroup || 'default') : 'default'),
|
|
597
644
|
selfHealingCommand: body.selfHealingCommand !== undefined ? String(body.selfHealingCommand).trim() : (current ? (current.selfHealingCommand || '') : ''),
|
|
598
645
|
autoDiagnose: body.autoDiagnose !== undefined ? Boolean(body.autoDiagnose) : (current ? Boolean(current.autoDiagnose) : false),
|
|
646
|
+
adaptive: body.adaptive !== undefined ? sanitizeAdaptiveConfig(body.adaptive) : (current ? current.adaptive : undefined),
|
|
647
|
+
adaptiveState: current ? current.adaptiveState : undefined,
|
|
599
648
|
};
|
|
600
649
|
}
|
|
601
650
|
|