@goodandready/dsh-cron 0.3.22 → 0.3.24
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/chat-start.js +13 -2
- package/lib/index.js +16 -2
- package/lib/models-handler.js +14 -6
- package/lib/routes.js +74 -0
- package/lib/runner-external.js +91 -6
- package/lib/runner.js +46 -7
- package/lib/runtimes.js +8 -2
- package/lib/store.js +171 -1
- package/lib/task-create.js +3 -1
- package/lib/task-schema.js +180 -0
- package/lib/task-transfer.js +5 -0
- package/package.json +11 -2
package/lib/chat-start.js
CHANGED
|
@@ -18,7 +18,12 @@ export async function chatStartHandler(ctx, req, res, parseJsonBody, sendJson, r
|
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
let agents = null;
|
|
22
|
+
try {
|
|
23
|
+
agents = (ctx && typeof ctx.get === 'function' ? ctx.get('agents') : null) || (ctx && ctx.agents);
|
|
24
|
+
} catch {
|
|
25
|
+
agents = null;
|
|
26
|
+
}
|
|
22
27
|
if (!agents || typeof agents.create !== 'function') {
|
|
23
28
|
sendJson(res, 500, { ok: false, error: 'The agents service is unavailable in DSH' });
|
|
24
29
|
return;
|
|
@@ -45,7 +50,13 @@ export async function chatStartHandler(ctx, req, res, parseJsonBody, sendJson, r
|
|
|
45
50
|
(ctx.logger || console).debug?.('[dsh-cron] @deepseek-ai/dsh-session SessionId unavailable, using string fallback:', err?.message || err);
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
|
|
53
|
+
let defaultSel = null;
|
|
54
|
+
try {
|
|
55
|
+
const adm = (ctx && typeof ctx.get === 'function' ? ctx.get('agentDefaultModel') : null) || (ctx && ctx.agentDefaultModel);
|
|
56
|
+
defaultSel = adm?.currentSelection?.();
|
|
57
|
+
} catch {
|
|
58
|
+
defaultSel = null;
|
|
59
|
+
}
|
|
49
60
|
const provider = body.provider || defaultSel?.provider;
|
|
50
61
|
const model = body.model || defaultSel?.model;
|
|
51
62
|
|
package/lib/index.js
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TaskSchema,
|
|
3
|
+
validateTaskRecord,
|
|
4
|
+
safeValidateTaskRecord,
|
|
5
|
+
validateTaskPatch,
|
|
6
|
+
} from './task-schema.js';
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
TaskSchema,
|
|
10
|
+
validateTaskRecord,
|
|
11
|
+
safeValidateTaskRecord,
|
|
12
|
+
validateTaskPatch,
|
|
13
|
+
};
|
|
1
14
|
import fs from 'node:fs';
|
|
2
15
|
import path from 'node:path';
|
|
3
16
|
import { bestEffort } from './best-effort.js';
|
|
4
17
|
import { setLogger, logger } from './logger.js';
|
|
5
18
|
import { registerPluginUpdater } from './updater.js';
|
|
6
19
|
import { credentialRefStatus } from './secrets.js';
|
|
7
|
-
import { TaskStore } from './store.js';
|
|
20
|
+
import { TaskStore, CURRENT_SCHEMA_VERSION, runStoreMigrations } from './store.js';
|
|
21
|
+
export { TaskStore, CURRENT_SCHEMA_VERSION, runStoreMigrations };
|
|
8
22
|
import { TaskScheduler, parseScheduleExpression } from './scheduler.js';
|
|
9
23
|
import { SessionRunner } from './runner.js';
|
|
10
24
|
import { getModelsHandler } from './models-handler.js';
|
|
@@ -95,7 +109,7 @@ export {
|
|
|
95
109
|
};
|
|
96
110
|
|
|
97
111
|
export const name = '@goodandready/dsh-cron';
|
|
98
|
-
export const inject = ['tools', 'webServer'
|
|
112
|
+
export const inject = ['tools', 'webServer'];
|
|
99
113
|
|
|
100
114
|
import { executeCreateTask } from './task-create.js';
|
|
101
115
|
import {
|
package/lib/models-handler.js
CHANGED
|
@@ -3,11 +3,18 @@ export async function getModelsHandler(ctx, req, res, sendJson) {
|
|
|
3
3
|
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
4
4
|
return;
|
|
5
5
|
}
|
|
6
|
-
|
|
6
|
+
let current = null;
|
|
7
|
+
try {
|
|
8
|
+
const adm = (ctx && typeof ctx.get === 'function' ? ctx.get('agentDefaultModel') : null) || (ctx && ctx.agentDefaultModel);
|
|
9
|
+
current = adm?.currentSelection?.() ?? null;
|
|
10
|
+
} catch {
|
|
11
|
+
current = null;
|
|
12
|
+
}
|
|
7
13
|
let providers = [];
|
|
8
14
|
try {
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
const llm = (ctx && typeof ctx.get === 'function' ? ctx.get('llm') : null) || (ctx && ctx.llm);
|
|
16
|
+
if (llm && typeof llm.listProviders === 'function') {
|
|
17
|
+
providers = (llm.listProviders() ?? []).map((p) => ({ id: p.id, name: p.name ?? p.id }));
|
|
11
18
|
}
|
|
12
19
|
} catch {
|
|
13
20
|
providers = [];
|
|
@@ -22,8 +29,9 @@ export async function getModelsHandler(ctx, req, res, sendJson) {
|
|
|
22
29
|
|
|
23
30
|
try {
|
|
24
31
|
let rows = [];
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
const llm = (ctx && typeof ctx.get === 'function' ? ctx.get('llm') : null) || (ctx && ctx.llm);
|
|
33
|
+
if (llm && typeof llm.listModels === 'function') {
|
|
34
|
+
rows = await llm.listModels(wanted);
|
|
27
35
|
}
|
|
28
36
|
sendJson(res, 200, {
|
|
29
37
|
current,
|
|
@@ -33,4 +41,4 @@ export async function getModelsHandler(ctx, req, res, sendJson) {
|
|
|
33
41
|
} catch {
|
|
34
42
|
sendJson(res, 200, { current, providers, models: [] });
|
|
35
43
|
}
|
|
36
|
-
}
|
|
44
|
+
}
|
package/lib/routes.js
CHANGED
|
@@ -22,6 +22,80 @@ const LOCALES_JSON = readFileSync(new URL('./locales.json', import.meta.url), 'u
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
export function registerRoutes(ctx, { store, scheduler, recommendations }) {
|
|
25
|
+
// Monthly history archives collection route (#391)
|
|
26
|
+
ctx.effect(() => ctx.webServer.register({
|
|
27
|
+
kind: 'exact',
|
|
28
|
+
path: '/dsh-cron/history/archives',
|
|
29
|
+
handler: (req, res) => {
|
|
30
|
+
if (req.method !== 'GET') {
|
|
31
|
+
res.writeHead(405, { 'content-type': 'text/plain; charset=utf-8' });
|
|
32
|
+
res.end('GET only');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
res.writeHead(200, {
|
|
36
|
+
'content-type': 'application/json; charset=utf-8',
|
|
37
|
+
'cache-control': 'no-store',
|
|
38
|
+
});
|
|
39
|
+
const archives = typeof store?.listMonthlyArchives === 'function' ? store.listMonthlyArchives() : [];
|
|
40
|
+
res.end(JSON.stringify({ ok: true, archives }));
|
|
41
|
+
}
|
|
42
|
+
}), 'dsh-cron: /history/archives');
|
|
43
|
+
|
|
44
|
+
// Specific monthly history archive route (#391)
|
|
45
|
+
ctx.effect(() => ctx.webServer.register({
|
|
46
|
+
kind: 'prefix',
|
|
47
|
+
path: '/dsh-cron/history/archive/',
|
|
48
|
+
handler: (req, res) => {
|
|
49
|
+
if (req.method !== 'GET') {
|
|
50
|
+
res.writeHead(405, { 'content-type': 'text/plain; charset=utf-8' });
|
|
51
|
+
res.end('GET only');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const url = new URL(req.url, 'http://127.0.0.1');
|
|
55
|
+
const month = url.pathname.replace(/^\/dsh-cron\/history\/archive\//, '').replace(/\/$/, '');
|
|
56
|
+
if (!/^\d{4}-\d{2}$/.test(month)) {
|
|
57
|
+
res.writeHead(400, { 'content-type': 'application/json; charset=utf-8' });
|
|
58
|
+
res.end(JSON.stringify({ ok: false, error: 'Invalid month format, expected YYYY-MM' }));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const archive = typeof store?.getMonthlyArchive === 'function' ? store.getMonthlyArchive(month) : null;
|
|
62
|
+
if (!archive) {
|
|
63
|
+
res.writeHead(404, { 'content-type': 'application/json; charset=utf-8' });
|
|
64
|
+
res.end(JSON.stringify({ ok: false, error: `Archive for month "${month}" not found` }));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
res.writeHead(200, {
|
|
68
|
+
'content-type': 'application/json; charset=utf-8',
|
|
69
|
+
'cache-control': 'public, max-age=3600',
|
|
70
|
+
});
|
|
71
|
+
res.end(JSON.stringify({ ok: true, month, archive }));
|
|
72
|
+
}
|
|
73
|
+
}), 'dsh-cron: /history/archive/:month');
|
|
74
|
+
|
|
75
|
+
// Health probe route (#384)
|
|
76
|
+
ctx.effect(() => ctx.webServer.register({
|
|
77
|
+
kind: 'exact',
|
|
78
|
+
path: '/dsh-cron/health',
|
|
79
|
+
handler: (req, res) => {
|
|
80
|
+
if (req.method !== 'GET') {
|
|
81
|
+
res.writeHead(405, { 'content-type': 'text/plain; charset=utf-8' });
|
|
82
|
+
res.end('GET only');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
res.writeHead(200, {
|
|
86
|
+
'content-type': 'application/json; charset=utf-8',
|
|
87
|
+
'cache-control': 'no-store'
|
|
88
|
+
});
|
|
89
|
+
const tasks = typeof store?.list === 'function' ? store.list().length : 0;
|
|
90
|
+
res.end(JSON.stringify({
|
|
91
|
+
status: 'ok',
|
|
92
|
+
plugin: '@goodandready/dsh-cron',
|
|
93
|
+
tasks,
|
|
94
|
+
uptimeSec: Math.floor(process.uptime()),
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
}), 'dsh-cron: /health');
|
|
98
|
+
|
|
25
99
|
// Client locales route (#374)
|
|
26
100
|
ctx.effect(() => ctx.webServer.register({
|
|
27
101
|
kind: 'exact',
|
package/lib/runner-external.js
CHANGED
|
@@ -176,11 +176,22 @@ export async function runExternal(task, runtime, signal, execOptions = {}, { ctx
|
|
|
176
176
|
return new Promise((resolve, reject) => {
|
|
177
177
|
let child = null;
|
|
178
178
|
let settled = false;
|
|
179
|
+
let timedOut = false;
|
|
179
180
|
let stdout = '';
|
|
180
181
|
let stderr = '';
|
|
181
|
-
|
|
182
|
+
let truncated = false;
|
|
183
|
+
// Issue #393: maxLogBytesPerRun cap (default 5MB)
|
|
184
|
+
const maxLogBytes = Number(task.maxLogBytesPerRun) > 0 ? Number(task.maxLogBytesPerRun) : 5 * 1024 * 1024;
|
|
182
185
|
const isPosix = process.platform !== 'win32';
|
|
183
186
|
|
|
187
|
+
// Issue #393: Execution timeout guardrail (default 15 minutes = 900,000ms, or task.executionTimeoutMs / timeoutSeconds)
|
|
188
|
+
const effectiveTimeoutMs = Number(task.executionTimeoutMs) > 0
|
|
189
|
+
? Number(task.executionTimeoutMs)
|
|
190
|
+
: (Number(task.timeoutSeconds) > 0 ? Number(task.timeoutSeconds) * 1000 : 15 * 60 * 1000);
|
|
191
|
+
|
|
192
|
+
let timeoutTimer = null;
|
|
193
|
+
let forceKillTimer = null;
|
|
194
|
+
|
|
184
195
|
const killProcessTree = (sig = 'SIGTERM') => {
|
|
185
196
|
terminateProcessTree(child, sig, {
|
|
186
197
|
isPosix,
|
|
@@ -190,13 +201,37 @@ export async function runExternal(task, runtime, signal, execOptions = {}, { ctx
|
|
|
190
201
|
});
|
|
191
202
|
};
|
|
192
203
|
|
|
204
|
+
const cleanupTimers = () => {
|
|
205
|
+
if (timeoutTimer) { clearTimeout(timeoutTimer); timeoutTimer = null; }
|
|
206
|
+
if (forceKillTimer) { clearTimeout(forceKillTimer); forceKillTimer = null; }
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
if (effectiveTimeoutMs > 0) {
|
|
210
|
+
timeoutTimer = setTimeout(() => {
|
|
211
|
+
if (settled) return;
|
|
212
|
+
timedOut = true;
|
|
213
|
+
logger.warn(`[dsh-cron] task "${task.title || task.id}" timed out after ${effectiveTimeoutMs}ms, sending SIGTERM`);
|
|
214
|
+
killProcessTree('SIGTERM');
|
|
215
|
+
forceKillTimer = setTimeout(() => {
|
|
216
|
+
if (!settled) {
|
|
217
|
+
logger.warn(`[dsh-cron] task "${task.title || task.id}" force killing with SIGKILL`);
|
|
218
|
+
killProcessTree('SIGKILL');
|
|
219
|
+
}
|
|
220
|
+
}, 3000);
|
|
221
|
+
if (typeof forceKillTimer.unref === 'function') forceKillTimer.unref();
|
|
222
|
+
}, effectiveTimeoutMs);
|
|
223
|
+
if (typeof timeoutTimer.unref === 'function') timeoutTimer.unref();
|
|
224
|
+
}
|
|
225
|
+
|
|
193
226
|
const abortHandler = () => {
|
|
227
|
+
cleanupTimers();
|
|
194
228
|
killProcessTree('SIGTERM');
|
|
195
229
|
setTimeout(() => killProcessTree('SIGKILL'), 300);
|
|
196
230
|
};
|
|
197
231
|
|
|
198
232
|
if (signal) {
|
|
199
233
|
if (signal.aborted) {
|
|
234
|
+
cleanupTimers();
|
|
200
235
|
return reject(signal.reason || new Error('Command aborted'));
|
|
201
236
|
}
|
|
202
237
|
signal.addEventListener('abort', abortHandler, { once: true });
|
|
@@ -218,23 +253,67 @@ export async function runExternal(task, runtime, signal, execOptions = {}, { ctx
|
|
|
218
253
|
if (invocation.file) {
|
|
219
254
|
child = spawn(invocation.file, invocation.args || [], options);
|
|
220
255
|
} else {
|
|
256
|
+
let command = invocation.shellCommand;
|
|
257
|
+
if (isPosix && Number(task.maxMemoryMb) > 0) {
|
|
258
|
+
command = `ulimit -v ${Number(task.maxMemoryMb) * 1024} 2>/dev/null || true; ${command}`;
|
|
259
|
+
}
|
|
221
260
|
const shellPath = isPosix ? (process.env.SHELL || '/bin/bash') : (process.env.ComSpec || 'cmd.exe');
|
|
222
|
-
const shellArgs = isPosix ? ['-c',
|
|
261
|
+
const shellArgs = isPosix ? ['-c', command] : ['/d', '/s', '/c', command];
|
|
223
262
|
child = spawn(shellPath, shellArgs, options);
|
|
224
263
|
}
|
|
225
264
|
|
|
226
265
|
child.stdout.on('data', (chunk) => {
|
|
227
|
-
|
|
228
|
-
|
|
266
|
+
const remaining = maxLogBytes - (stdout.length + stderr.length);
|
|
267
|
+
const text = chunk.toString('utf-8');
|
|
268
|
+
if (remaining > 0) {
|
|
269
|
+
if (text.length <= remaining) {
|
|
270
|
+
stdout += text;
|
|
271
|
+
if (!truncated) logStreamer.appendLog(task.id, text, 'stdout');
|
|
272
|
+
} else {
|
|
273
|
+
stdout += text.slice(0, remaining);
|
|
274
|
+
if (!truncated) {
|
|
275
|
+
truncated = true;
|
|
276
|
+
const note = `\n[TRUNCATED: log output exceeded maxLogBytesPerRun (${Math.round(maxLogBytes / 1024 / 1024)}MB)]`;
|
|
277
|
+
stdout += note;
|
|
278
|
+
logStreamer.appendLog(task.id, text.slice(0, remaining) + note, 'stdout');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} else if (!truncated) {
|
|
282
|
+
truncated = true;
|
|
283
|
+
const note = `\n[TRUNCATED: log output exceeded maxLogBytesPerRun (${Math.round(maxLogBytes / 1024 / 1024)}MB)]`;
|
|
284
|
+
stdout += note;
|
|
285
|
+
logStreamer.appendLog(task.id, note, 'stdout');
|
|
286
|
+
}
|
|
229
287
|
});
|
|
288
|
+
|
|
230
289
|
child.stderr.on('data', (chunk) => {
|
|
231
|
-
|
|
232
|
-
|
|
290
|
+
const remaining = maxLogBytes - (stdout.length + stderr.length);
|
|
291
|
+
const text = chunk.toString('utf-8');
|
|
292
|
+
if (remaining > 0) {
|
|
293
|
+
if (text.length <= remaining) {
|
|
294
|
+
stderr += text;
|
|
295
|
+
if (!truncated) logStreamer.appendLog(task.id, text, 'stderr');
|
|
296
|
+
} else {
|
|
297
|
+
stderr += text.slice(0, remaining);
|
|
298
|
+
if (!truncated) {
|
|
299
|
+
truncated = true;
|
|
300
|
+
const note = `\n[TRUNCATED: log output exceeded maxLogBytesPerRun (${Math.round(maxLogBytes / 1024 / 1024)}MB)]`;
|
|
301
|
+
stderr += note;
|
|
302
|
+
logStreamer.appendLog(task.id, text.slice(0, remaining) + note, 'stderr');
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} else if (!truncated) {
|
|
306
|
+
truncated = true;
|
|
307
|
+
const note = `\n[TRUNCATED: log output exceeded maxLogBytesPerRun (${Math.round(maxLogBytes / 1024 / 1024)}MB)]`;
|
|
308
|
+
stderr += note;
|
|
309
|
+
logStreamer.appendLog(task.id, note, 'stderr');
|
|
310
|
+
}
|
|
233
311
|
});
|
|
234
312
|
|
|
235
313
|
child.on('error', (err) => {
|
|
236
314
|
if (settled) return;
|
|
237
315
|
settled = true;
|
|
316
|
+
cleanupTimers();
|
|
238
317
|
if (signal) signal.removeEventListener('abort', abortHandler);
|
|
239
318
|
logStreamer.completeStream(task.id, { status: 'error', error: err.message, exitCode: 1 });
|
|
240
319
|
reject(err);
|
|
@@ -243,8 +322,14 @@ export async function runExternal(task, runtime, signal, execOptions = {}, { ctx
|
|
|
243
322
|
child.on('close', (code, sig) => {
|
|
244
323
|
if (settled) return;
|
|
245
324
|
settled = true;
|
|
325
|
+
cleanupTimers();
|
|
246
326
|
if (signal) signal.removeEventListener('abort', abortHandler);
|
|
247
327
|
|
|
328
|
+
if (timedOut) {
|
|
329
|
+
logStreamer.completeStream(task.id, { status: 'timeout', error: `Execution timed out after ${effectiveTimeoutMs}ms`, exitCode: 124 });
|
|
330
|
+
return reject(new Error(`Execution timed out after ${effectiveTimeoutMs}ms (SIGTERM/SIGKILL sent)`));
|
|
331
|
+
}
|
|
332
|
+
|
|
248
333
|
if (signal && signal.aborted) {
|
|
249
334
|
return reject(signal.reason || new Error('Command aborted'));
|
|
250
335
|
}
|
package/lib/runner.js
CHANGED
|
@@ -79,6 +79,40 @@ export class SessionRunner {
|
|
|
79
79
|
this.store = store;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
_getAgentsService() {
|
|
83
|
+
if (!this.ctx) return null;
|
|
84
|
+
try {
|
|
85
|
+
if (typeof this.ctx.get === 'function') {
|
|
86
|
+
const svc = this.ctx.get('agents');
|
|
87
|
+
if (svc) return svc;
|
|
88
|
+
}
|
|
89
|
+
} catch (_err) {
|
|
90
|
+
// service unavailable via ctx.get
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
return this.ctx.agents || null;
|
|
94
|
+
} catch (_err) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
_getSessionService() {
|
|
100
|
+
if (!this.ctx) return null;
|
|
101
|
+
try {
|
|
102
|
+
if (typeof this.ctx.get === 'function') {
|
|
103
|
+
const svc = this.ctx.get('session');
|
|
104
|
+
if (svc) return svc;
|
|
105
|
+
}
|
|
106
|
+
} catch (_err) {
|
|
107
|
+
// service unavailable via ctx.get
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
return this.ctx.session || null;
|
|
111
|
+
} catch (_err) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
82
116
|
async execute(task, options = {}) {
|
|
83
117
|
const timeoutSeconds = Number(task.timeoutSeconds) > 0 ? Number(task.timeoutSeconds) : 1800;
|
|
84
118
|
const timeoutMs = timeoutSeconds * 1000;
|
|
@@ -234,12 +268,14 @@ export class SessionRunner {
|
|
|
234
268
|
}
|
|
235
269
|
|
|
236
270
|
// 2. Agent session, when the harness exposes one
|
|
237
|
-
|
|
271
|
+
const agentsSvc = this._getAgentsService();
|
|
272
|
+
if (agentsSvc && typeof agentsSvc.create === 'function') {
|
|
238
273
|
return this._runAgentSession(effectiveTask, runtime, signal, options);
|
|
239
274
|
}
|
|
240
275
|
|
|
241
276
|
// 3. Legacy session service
|
|
242
|
-
|
|
277
|
+
const sessionSvc = this._getSessionService();
|
|
278
|
+
if (sessionSvc && typeof sessionSvc.create === 'function') {
|
|
243
279
|
return this._runLegacySession(effectiveTask, options);
|
|
244
280
|
}
|
|
245
281
|
|
|
@@ -270,12 +306,14 @@ export class SessionRunner {
|
|
|
270
306
|
}
|
|
271
307
|
|
|
272
308
|
// 2. Agent session, when the harness exposes one
|
|
273
|
-
|
|
309
|
+
const agentsSvc2 = this._getAgentsService();
|
|
310
|
+
if (agentsSvc2 && typeof agentsSvc2.create === 'function') {
|
|
274
311
|
return this._runAgentSession(task, runtime, signal, options);
|
|
275
312
|
}
|
|
276
313
|
|
|
277
314
|
// 3. Legacy session service
|
|
278
|
-
|
|
315
|
+
const sessionSvc2 = this._getSessionService();
|
|
316
|
+
if (sessionSvc2 && typeof sessionSvc2.create === 'function') {
|
|
279
317
|
return this._runLegacySession(task, options);
|
|
280
318
|
}
|
|
281
319
|
|
|
@@ -484,9 +522,10 @@ export class SessionRunner {
|
|
|
484
522
|
|
|
485
523
|
// If targetSessionId is set, attempt to resume the existing session (#143, #346, GH #9)
|
|
486
524
|
let resumeFailure = null;
|
|
487
|
-
|
|
525
|
+
const agents = this._getAgentsService();
|
|
526
|
+
if (prep.targetSessionId && agents && typeof agents.resume === 'function') {
|
|
488
527
|
try {
|
|
489
|
-
handle = await
|
|
528
|
+
handle = await agents.resume({
|
|
490
529
|
sessionId: prep.targetSessionId,
|
|
491
530
|
resumeSessionId: prep.targetSessionId,
|
|
492
531
|
meta: {
|
|
@@ -508,7 +547,7 @@ export class SessionRunner {
|
|
|
508
547
|
// Create new session if not resumed
|
|
509
548
|
if (!handle) {
|
|
510
549
|
try {
|
|
511
|
-
handle = await
|
|
550
|
+
handle = await agents.create({
|
|
512
551
|
sessionId: prep.mintSessionId(),
|
|
513
552
|
meta: {
|
|
514
553
|
cwd: prep.cwd,
|
package/lib/runtimes.js
CHANGED
|
@@ -48,12 +48,16 @@ function looksLikeScriptFile(token, extensions) {
|
|
|
48
48
|
export function buildNodeInvocation(task, cwd) {
|
|
49
49
|
const nodeExecutable = task.nodePath || process.execPath;
|
|
50
50
|
const { path: filePath, args } = splitCommandLine(task.prompt);
|
|
51
|
+
const flags = [];
|
|
52
|
+
if (Number(task.maxMemoryMb) > 0) {
|
|
53
|
+
flags.push(`--max-old-space-size=${Number(task.maxMemoryMb)}`);
|
|
54
|
+
}
|
|
51
55
|
if (looksLikeScriptFile(filePath, ['.js', '.mjs', '.cjs'])) {
|
|
52
|
-
return { file: nodeExecutable, args: [filePath, ...args] };
|
|
56
|
+
return { file: nodeExecutable, args: [...flags, filePath, ...args] };
|
|
53
57
|
}
|
|
54
58
|
const code = String(task.prompt || '');
|
|
55
59
|
const isEsm = /\bimport\s|\bexport\s/.test(code);
|
|
56
|
-
const nodeArgs = isEsm ? ['--input-type=module', '-e', code] : ['-e', code];
|
|
60
|
+
const nodeArgs = isEsm ? [...flags, '--input-type=module', '-e', code] : [...flags, '-e', code];
|
|
57
61
|
return { file: nodeExecutable, args: nodeArgs };
|
|
58
62
|
}
|
|
59
63
|
|
|
@@ -147,6 +151,8 @@ export function buildDockerInvocation(task, explicitEnv) {
|
|
|
147
151
|
if (value === null || value === undefined) continue;
|
|
148
152
|
args.push('-e', `${key}=${value}`);
|
|
149
153
|
}
|
|
154
|
+
if (Number(task.maxMemoryMb) > 0) args.push(`--memory=${Number(task.maxMemoryMb)}m`);
|
|
155
|
+
if (Number(task.cpus) > 0) args.push(`--cpus=${Number(task.cpus)}`);
|
|
150
156
|
if (task.cwd) args.push('-v', `${task.cwd}:/workspace`, '-w', '/workspace');
|
|
151
157
|
args.push(image, 'sh', '-lc', String(task.prompt || 'true'));
|
|
152
158
|
return { file: 'docker', args, env: envMap };
|
package/lib/store.js
CHANGED
|
@@ -4,6 +4,7 @@ import { ArtifactManager } from './artifacts.js';
|
|
|
4
4
|
import { logger } from './logger.js';
|
|
5
5
|
import { bestEffort } from './best-effort.js';
|
|
6
6
|
import fs from 'node:fs';
|
|
7
|
+
import { gzipSync, gunzipSync } from 'node:zlib';
|
|
7
8
|
import { pipeline } from 'node:stream/promises';
|
|
8
9
|
import path from 'node:path';
|
|
9
10
|
import { getDshDefaultTelegramCredentials } from './telegram.js';
|
|
@@ -17,6 +18,36 @@ import { getDshDefaultTelegramCredentials } from './telegram.js';
|
|
|
17
18
|
* home's data directory, which is how an isolated test cycle once leaked a
|
|
18
19
|
* task into the wrong profile.
|
|
19
20
|
*/
|
|
21
|
+
|
|
22
|
+
export const CURRENT_SCHEMA_VERSION = 2;
|
|
23
|
+
|
|
24
|
+
export function runStoreMigrations(data, fromVersion = 1, toVersion = CURRENT_SCHEMA_VERSION) {
|
|
25
|
+
if (!data || typeof data !== 'object') return data;
|
|
26
|
+
let current = { ...data };
|
|
27
|
+
|
|
28
|
+
// Migration from version 1 to 2 (Issue #390 & #393)
|
|
29
|
+
if (fromVersion < 2 && toVersion >= 2) {
|
|
30
|
+
if (Array.isArray(current.tasks)) {
|
|
31
|
+
current.tasks = current.tasks.map((task) => {
|
|
32
|
+
if (!task || typeof task !== 'object') return task;
|
|
33
|
+
return {
|
|
34
|
+
...task,
|
|
35
|
+
maxMemoryMb: task.maxMemoryMb !== undefined ? Number(task.maxMemoryMb) : 0,
|
|
36
|
+
executionTimeoutMs: task.executionTimeoutMs !== undefined
|
|
37
|
+
? Number(task.executionTimeoutMs)
|
|
38
|
+
: (Number(task.timeoutSeconds) > 0 ? Number(task.timeoutSeconds) * 1000 : 900000),
|
|
39
|
+
maxLogBytesPerRun: task.maxLogBytesPerRun !== undefined ? Number(task.maxLogBytesPerRun) : (5 * 1024 * 1024),
|
|
40
|
+
cpus: task.cpus !== undefined ? Number(task.cpus) : 0,
|
|
41
|
+
maxArchiveMonths: task.maxArchiveMonths !== undefined ? Number(task.maxArchiveMonths) : 6,
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
current.schemaVersion = 2;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return current;
|
|
49
|
+
}
|
|
50
|
+
|
|
20
51
|
export function getDefaultStorePath() {
|
|
21
52
|
const base = process.env.DSH_DATA_DIR
|
|
22
53
|
|| (process.env.DSH_HOME
|
|
@@ -64,7 +95,21 @@ export class TaskStore {
|
|
|
64
95
|
const bakPath = `${this.filePath}.bak`;
|
|
65
96
|
|
|
66
97
|
const loadData = (raw) => {
|
|
67
|
-
|
|
98
|
+
let data = JSON.parse(raw);
|
|
99
|
+
const storedVersion = Number(data.schemaVersion) || 1;
|
|
100
|
+
if (storedVersion < CURRENT_SCHEMA_VERSION) {
|
|
101
|
+
// Pre-migration backup (#390)
|
|
102
|
+
try {
|
|
103
|
+
const preBakPath = `${this.filePath}.bak-v${storedVersion}-${Date.now()}`;
|
|
104
|
+
fs.writeFileSync(preBakPath, raw, { encoding: 'utf-8', mode: 0o600 });
|
|
105
|
+
bestEffort('chmod-pre-bak', () => fs.chmodSync(preBakPath, 0o600));
|
|
106
|
+
logger.info(`[dsh-cron] pre-migration backup saved to ${preBakPath}`);
|
|
107
|
+
} catch (bakErr) {
|
|
108
|
+
logger.warn('[dsh-cron] failed to write pre-migration backup:', bakErr.message);
|
|
109
|
+
}
|
|
110
|
+
data = runStoreMigrations(data, storedVersion, CURRENT_SCHEMA_VERSION);
|
|
111
|
+
this._needsMigrationFlush = true;
|
|
112
|
+
}
|
|
68
113
|
this.tasks.clear();
|
|
69
114
|
this.history.clear();
|
|
70
115
|
if (Array.isArray(data.tasks)) {
|
|
@@ -151,6 +196,11 @@ export class TaskStore {
|
|
|
151
196
|
logger.error('[dsh-cron] failed to restore tasks store from .bak:', bakErr.message);
|
|
152
197
|
}
|
|
153
198
|
}
|
|
199
|
+
if (this._needsMigrationFlush) {
|
|
200
|
+
this._needsMigrationFlush = false;
|
|
201
|
+
this.scheduleSave(100);
|
|
202
|
+
}
|
|
203
|
+
bestEffort('prune-old-archives', () => this._pruneOldMonthlyArchives(dir, 6));
|
|
154
204
|
}
|
|
155
205
|
|
|
156
206
|
/**
|
|
@@ -201,6 +251,7 @@ export class TaskStore {
|
|
|
201
251
|
}
|
|
202
252
|
return {
|
|
203
253
|
version: 1,
|
|
254
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
204
255
|
updatedAt: new Date().toISOString(),
|
|
205
256
|
tasks: tasksSnapshot,
|
|
206
257
|
history: historySnapshot,
|
|
@@ -358,6 +409,7 @@ export class TaskStore {
|
|
|
358
409
|
}
|
|
359
410
|
|
|
360
411
|
yield '{"version":' + JSON.stringify(data.version || 1);
|
|
412
|
+
yield ',"schemaVersion":' + JSON.stringify(data.schemaVersion || CURRENT_SCHEMA_VERSION);
|
|
361
413
|
yield ',"updatedAt":' + JSON.stringify(data.updatedAt || new Date().toISOString());
|
|
362
414
|
yield ',"settings":' + JSON.stringify(data.settings || {});
|
|
363
415
|
|
|
@@ -651,6 +703,11 @@ export class TaskStore {
|
|
|
651
703
|
? (task.onlyOnFailure === null || task.onlyOnFailure === 'inherit' ? undefined : Boolean(task.onlyOnFailure))
|
|
652
704
|
: prev.onlyOnFailure,
|
|
653
705
|
timeoutSeconds: task.timeoutSeconds !== undefined ? Number(task.timeoutSeconds) : (prev.timeoutSeconds || 1800),
|
|
706
|
+
maxMemoryMb: task.maxMemoryMb !== undefined ? Number(task.maxMemoryMb) : (prev.maxMemoryMb || 0),
|
|
707
|
+
executionTimeoutMs: task.executionTimeoutMs !== undefined ? Number(task.executionTimeoutMs) : (prev.executionTimeoutMs || 0),
|
|
708
|
+
maxLogBytesPerRun: task.maxLogBytesPerRun !== undefined ? Number(task.maxLogBytesPerRun) : (prev.maxLogBytesPerRun || (5 * 1024 * 1024)),
|
|
709
|
+
cpus: task.cpus !== undefined ? Number(task.cpus) : (prev.cpus || 0),
|
|
710
|
+
maxArchiveMonths: task.maxArchiveMonths !== undefined ? Number(task.maxArchiveMonths) : (prev.maxArchiveMonths || 6),
|
|
654
711
|
overlapPolicy: task.overlapPolicy !== undefined ? String(task.overlapPolicy) : (prev.overlapPolicy || 'skip'),
|
|
655
712
|
kanbanMode: task.kanbanMode !== undefined ? String(task.kanbanMode) : (prev.kanbanMode || 'none'), // 'none' | 'on_failure' | 'always'
|
|
656
713
|
onSuccess: task.onSuccess !== undefined ? String(task.onSuccess).trim() : (prev.onSuccess || ''),
|
|
@@ -922,11 +979,124 @@ export class TaskStore {
|
|
|
922
979
|
bestEffort('chmod-tmp', () => fs.chmodSync(tmp, 0o600));
|
|
923
980
|
fs.renameSync(tmp, archivePath);
|
|
924
981
|
bestEffort('chmod-archive', () => fs.chmodSync(archivePath, 0o600));
|
|
982
|
+
|
|
983
|
+
// Partition runs into monthly buckets for compressed archives (#391)
|
|
984
|
+
const monthlyBuckets = new Map();
|
|
985
|
+
for (const run of sanitizedOverflow) {
|
|
986
|
+
const at = run.at || run.timestamp || run.startedAt || Date.now();
|
|
987
|
+
const date = new Date(at);
|
|
988
|
+
const monthKey = !isNaN(date.getTime())
|
|
989
|
+
? `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`
|
|
990
|
+
: new Date().toISOString().slice(0, 7);
|
|
991
|
+
if (!monthlyBuckets.has(monthKey)) {
|
|
992
|
+
monthlyBuckets.set(monthKey, []);
|
|
993
|
+
}
|
|
994
|
+
monthlyBuckets.get(monthKey).push(run);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
for (const [monthKey, runsForMonth] of monthlyBuckets.entries()) {
|
|
998
|
+
const gzPath = path.join(dir, `tasks-history-${monthKey}.json.gz`);
|
|
999
|
+
let monthArchive = {};
|
|
1000
|
+
if (fs.existsSync(gzPath)) {
|
|
1001
|
+
try {
|
|
1002
|
+
const rawGz = fs.readFileSync(gzPath);
|
|
1003
|
+
const decompressed = gunzipSync(rawGz).toString('utf-8');
|
|
1004
|
+
monthArchive = JSON.parse(decompressed) || {};
|
|
1005
|
+
} catch (gzReadErr) {
|
|
1006
|
+
logger.warn(`[dsh-cron] failed to read existing gzip archive for ${monthKey}:`, gzReadErr.message);
|
|
1007
|
+
monthArchive = {};
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
if (!Array.isArray(monthArchive[id])) {
|
|
1011
|
+
monthArchive[id] = [];
|
|
1012
|
+
}
|
|
1013
|
+
monthArchive[id].push(...runsForMonth);
|
|
1014
|
+
const compressed = gzipSync(Buffer.from(JSON.stringify(monthArchive), 'utf-8'), { level: 9 });
|
|
1015
|
+
const randGz = Math.random().toString(36).slice(2, 8);
|
|
1016
|
+
const tmpGz = `${gzPath}.tmp.${process.pid}_${Date.now()}_${randGz}`;
|
|
1017
|
+
fs.writeFileSync(tmpGz, compressed, { mode: 0o600 });
|
|
1018
|
+
bestEffort('chmod-tmp-gz', () => fs.chmodSync(tmpGz, 0o600));
|
|
1019
|
+
fs.renameSync(tmpGz, gzPath);
|
|
1020
|
+
bestEffort('chmod-archive-gz', () => fs.chmodSync(gzPath, 0o600));
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
this._pruneOldMonthlyArchives(dir, 6);
|
|
925
1024
|
} catch (archiveErr) {
|
|
926
1025
|
logger.error('[dsh-cron] history archive error:', archiveErr.message);
|
|
927
1026
|
}
|
|
928
1027
|
}
|
|
929
1028
|
|
|
1029
|
+
listMonthlyArchives() {
|
|
1030
|
+
try {
|
|
1031
|
+
const dir = path.dirname(this.filePath);
|
|
1032
|
+
if (!fs.existsSync(dir)) return [];
|
|
1033
|
+
const files = fs.readdirSync(dir);
|
|
1034
|
+
const archives = [];
|
|
1035
|
+
for (const file of files) {
|
|
1036
|
+
const match = file.match(/^tasks-history-(\d{4}-\d{2})\.json\.gz$/);
|
|
1037
|
+
if (match) {
|
|
1038
|
+
const month = match[1];
|
|
1039
|
+
const fullPath = path.join(dir, file);
|
|
1040
|
+
try {
|
|
1041
|
+
const stat = fs.statSync(fullPath);
|
|
1042
|
+
archives.push({
|
|
1043
|
+
month,
|
|
1044
|
+
filename: file,
|
|
1045
|
+
sizeBytes: stat.size,
|
|
1046
|
+
mtime: stat.mtime.toISOString(),
|
|
1047
|
+
path: fullPath,
|
|
1048
|
+
});
|
|
1049
|
+
} catch (_err) {
|
|
1050
|
+
// ignore unreadable stat
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
return archives.sort((a, b) => b.month.localeCompare(a.month));
|
|
1055
|
+
} catch (err) {
|
|
1056
|
+
logger.error('[dsh-cron] listMonthlyArchives error:', err.message);
|
|
1057
|
+
return [];
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
getMonthlyArchive(month) {
|
|
1062
|
+
if (!month || !/^\d{4}-\d{2}$/.test(month)) return null;
|
|
1063
|
+
try {
|
|
1064
|
+
const dir = path.dirname(this.filePath);
|
|
1065
|
+
const gzPath = path.join(dir, `tasks-history-${month}.json.gz`);
|
|
1066
|
+
if (!fs.existsSync(gzPath)) return null;
|
|
1067
|
+
const rawGz = fs.readFileSync(gzPath);
|
|
1068
|
+
const decompressed = gunzipSync(rawGz).toString('utf-8');
|
|
1069
|
+
return JSON.parse(decompressed);
|
|
1070
|
+
} catch (err) {
|
|
1071
|
+
logger.error(`[dsh-cron] getMonthlyArchive error for ${month}:`, err.message);
|
|
1072
|
+
return null;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
_pruneOldMonthlyArchives(dir, maxMonths = 6) {
|
|
1077
|
+
try {
|
|
1078
|
+
if (!fs.existsSync(dir) || maxMonths < 0) return;
|
|
1079
|
+
const files = fs.readdirSync(dir);
|
|
1080
|
+
const monthlyFiles = [];
|
|
1081
|
+
for (const file of files) {
|
|
1082
|
+
const match = file.match(/^tasks-history-(\d{4}-\d{2})\.json\.gz$/);
|
|
1083
|
+
if (match) {
|
|
1084
|
+
monthlyFiles.push({ month: match[1], file, fullPath: path.join(dir, file) });
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
monthlyFiles.sort((a, b) => b.month.localeCompare(a.month));
|
|
1088
|
+
if (monthlyFiles.length > maxMonths) {
|
|
1089
|
+
const toDelete = monthlyFiles.slice(maxMonths);
|
|
1090
|
+
for (const item of toDelete) {
|
|
1091
|
+
bestEffort('delete-old-archive', () => fs.unlinkSync(item.fullPath));
|
|
1092
|
+
logger.info(`[dsh-cron] pruned old monthly history archive: ${item.file}`);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
} catch (err) {
|
|
1096
|
+
bestEffort('log-prune-err', () => logger.debug?.(`[dsh-cron] prune old monthly archives error: ${err.message}`));
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
930
1100
|
getHistory(id, limit = 20) {
|
|
931
1101
|
const runs = this.history.get(id) || [];
|
|
932
1102
|
return runs.slice(0, limit);
|
package/lib/task-create.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateTaskRecord } from './task-schema.js';
|
|
1
2
|
import { sanitizeSwarmConfig } from './swarm.js';
|
|
2
3
|
import { sanitizeBranchRules } from './branching.js';
|
|
3
4
|
import { sanitizePromptHistory } from './prompt-evolver.js';
|
|
@@ -132,7 +133,8 @@ export function executeCreateTask(store, scheduler, args) {
|
|
|
132
133
|
throw new Error(dagCheck.error);
|
|
133
134
|
}
|
|
134
135
|
|
|
135
|
-
const
|
|
136
|
+
const validTask = validateTaskRecord(taskData);
|
|
137
|
+
const task = store.set(validTask);
|
|
136
138
|
scheduler.scheduleTask(task);
|
|
137
139
|
return {
|
|
138
140
|
success: true,
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import z from '@deepseek-ai/schemastery';
|
|
2
|
+
import { normalizeTaskType } from './runtimes.js';
|
|
3
|
+
import { validateTaskType, PATCHABLE_TASK_FIELDS } from './task-transfer.js';
|
|
4
|
+
import { CHANNEL_IDS } from './channels.js';
|
|
5
|
+
|
|
6
|
+
export const TaskSchema = z.object({
|
|
7
|
+
id: z.string(),
|
|
8
|
+
title: z.string().required(),
|
|
9
|
+
schedule: z.string().required(),
|
|
10
|
+
scheduleText: z.string(),
|
|
11
|
+
type: z.string().default('llm'),
|
|
12
|
+
prompt: z.string().default(''),
|
|
13
|
+
delivery: z.string().default('isolated'),
|
|
14
|
+
status: z.string().default('active'),
|
|
15
|
+
provider: z.string(),
|
|
16
|
+
model: z.string(),
|
|
17
|
+
fallbackProvider: z.string(),
|
|
18
|
+
fallbackModel: z.string(),
|
|
19
|
+
silentRule: z.string().default(''),
|
|
20
|
+
inspectOnFailure: z.boolean().default(false),
|
|
21
|
+
agentPreset: z.string().default(''),
|
|
22
|
+
targetSessionId: z.string().default(''),
|
|
23
|
+
targetSessionReset: z.string().default('never'),
|
|
24
|
+
sessionTitle: z.string().default(''),
|
|
25
|
+
timezone: z.string().default(''),
|
|
26
|
+
misfirePolicy: z.string().default('skip'),
|
|
27
|
+
maxRetries: z.number().default(0),
|
|
28
|
+
retryBackoffMs: z.number().default(30000),
|
|
29
|
+
permissionPreset: z.string().default('default'),
|
|
30
|
+
captureGitDiff: z.boolean(),
|
|
31
|
+
kanbanMode: z.string().default('none'),
|
|
32
|
+
channels: z.array(z.string()).default([]),
|
|
33
|
+
template: z.string().default(''),
|
|
34
|
+
env: z.any(),
|
|
35
|
+
cwd: z.string().default(''),
|
|
36
|
+
workspaceId: z.string().default(''),
|
|
37
|
+
worktree: z.boolean().default(false),
|
|
38
|
+
keepWorktree: z.boolean().default(false),
|
|
39
|
+
httpMethod: z.string().default('GET'),
|
|
40
|
+
httpUrl: z.string().default(''),
|
|
41
|
+
httpHeaders: z.any(),
|
|
42
|
+
httpBody: z.string().default(''),
|
|
43
|
+
sshProfileId: z.string().default(''),
|
|
44
|
+
sshTarget: z.string().default(''),
|
|
45
|
+
sshPort: z.number().default(0),
|
|
46
|
+
sshKeyPath: z.string().default(''),
|
|
47
|
+
dockerImage: z.string().default(''),
|
|
48
|
+
pythonPath: z.string().default(''),
|
|
49
|
+
nodePath: z.string().default(''),
|
|
50
|
+
skillName: z.string().default(''),
|
|
51
|
+
workflowName: z.string().default(''),
|
|
52
|
+
webhookEnabled: z.boolean().default(false),
|
|
53
|
+
webhookToken: z.string(),
|
|
54
|
+
webhookSecret: z.string(),
|
|
55
|
+
webhookDebounceMs: z.number(),
|
|
56
|
+
webhookTrigger: z.any(),
|
|
57
|
+
onSuccess: z.string().default(''),
|
|
58
|
+
onFailure: z.string().default(''),
|
|
59
|
+
heartbeatIntervalSeconds: z.number().default(0),
|
|
60
|
+
gracePeriodSeconds: z.number().default(300),
|
|
61
|
+
preflightType: z.string().default('none'),
|
|
62
|
+
preflightTarget: z.string().default(''),
|
|
63
|
+
preflightCommand: z.string(),
|
|
64
|
+
preflightScript: z.string(),
|
|
65
|
+
concurrencyGroup: z.string().default('default'),
|
|
66
|
+
priority: z.number().default(5),
|
|
67
|
+
selfHealingCommand: z.string().default(''),
|
|
68
|
+
autoDiagnose: z.boolean().default(false),
|
|
69
|
+
notifyTelegram: z.boolean(),
|
|
70
|
+
onlyOnFailure: z.boolean(),
|
|
71
|
+
timeoutSeconds: z.number().default(1800),
|
|
72
|
+
overlapPolicy: z.string().default('skip'),
|
|
73
|
+
oneShot: z.boolean().default(false),
|
|
74
|
+
targetTimestamp: z.number(),
|
|
75
|
+
nextRunAt: z.number(),
|
|
76
|
+
costLimitUsd: z.number(),
|
|
77
|
+
dailyCostLimitUsd: z.number(),
|
|
78
|
+
tokenLimit: z.number(),
|
|
79
|
+
pausedReason: z.string(),
|
|
80
|
+
validators: z.any(),
|
|
81
|
+
requireApproval: z.boolean().default(false),
|
|
82
|
+
fileWatch: z.any(),
|
|
83
|
+
dependsOn: z.array(z.string()).default([]),
|
|
84
|
+
joinPolicy: z.string().default('all_success'),
|
|
85
|
+
adaptive: z.any(),
|
|
86
|
+
adaptiveState: z.any(),
|
|
87
|
+
promptVersion: z.number(),
|
|
88
|
+
promptHistory: z.any(),
|
|
89
|
+
branchRules: z.any(),
|
|
90
|
+
elseTask: z.string(),
|
|
91
|
+
autoHeal: z.boolean(),
|
|
92
|
+
selfHealing: z.any(),
|
|
93
|
+
revisions: z.any(),
|
|
94
|
+
previousPrompt: z.string(),
|
|
95
|
+
previousScript: z.string(),
|
|
96
|
+
swarm: z.any(),
|
|
97
|
+
gitDiff: z.any(),
|
|
98
|
+
maxMemoryMb: z.number().min(0).default(0),
|
|
99
|
+
executionTimeoutMs: z.number().min(0).default(900000),
|
|
100
|
+
maxLogBytesPerRun: z.number().min(0).default(5 * 1024 * 1024),
|
|
101
|
+
cpus: z.number().min(0).default(0),
|
|
102
|
+
maxArchiveMonths: z.number().min(1).default(6),
|
|
103
|
+
schemaVersion: z.number().default(2),
|
|
104
|
+
createdAt: z.number(),
|
|
105
|
+
updatedAt: z.number(),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
export function validateTaskRecord(data) {
|
|
109
|
+
if (!data || typeof data !== 'object') {
|
|
110
|
+
throw new TypeError('Task record must be an object');
|
|
111
|
+
}
|
|
112
|
+
if (data.title !== undefined && typeof data.title !== 'string') {
|
|
113
|
+
throw new Error('Task schema validation failed: title must be a string');
|
|
114
|
+
}
|
|
115
|
+
const title = String(data.title || '').trim();
|
|
116
|
+
if (!title) {
|
|
117
|
+
throw new Error('Task title cannot be empty');
|
|
118
|
+
}
|
|
119
|
+
if (data.schedule !== undefined && typeof data.schedule !== 'string') {
|
|
120
|
+
throw new Error('Task schema validation failed: schedule must be a string');
|
|
121
|
+
}
|
|
122
|
+
const schedule = String(data.schedule || '').trim();
|
|
123
|
+
if (!schedule) {
|
|
124
|
+
throw new Error('Task schedule cannot be empty');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const cleanType = normalizeTaskType(data.type);
|
|
128
|
+
const typeError = validateTaskType(cleanType, data);
|
|
129
|
+
if (typeError) {
|
|
130
|
+
throw new Error(typeError);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let validated;
|
|
134
|
+
try {
|
|
135
|
+
validated = TaskSchema({
|
|
136
|
+
...data,
|
|
137
|
+
title,
|
|
138
|
+
schedule,
|
|
139
|
+
type: cleanType,
|
|
140
|
+
});
|
|
141
|
+
} catch (err) {
|
|
142
|
+
throw new Error(`Task schema validation failed: ${err.message}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Filter channels to known channel ids if present
|
|
146
|
+
if (Array.isArray(validated.channels)) {
|
|
147
|
+
validated.channels = validated.channels.filter((c) => CHANNEL_IDS.includes(c));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Strip undefined keys
|
|
151
|
+
for (const k of Object.keys(validated)) {
|
|
152
|
+
if (validated[k] === undefined) {
|
|
153
|
+
delete validated[k];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return validated;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function safeValidateTaskRecord(data) {
|
|
161
|
+
try {
|
|
162
|
+
const task = validateTaskRecord(data);
|
|
163
|
+
return { ok: true, task };
|
|
164
|
+
} catch (err) {
|
|
165
|
+
return { ok: false, error: err.message };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function validateTaskPatch(patch) {
|
|
170
|
+
if (!patch || typeof patch !== 'object') {
|
|
171
|
+
return { ok: false, error: 'Task patch must be an object' };
|
|
172
|
+
}
|
|
173
|
+
const clean = {};
|
|
174
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
175
|
+
if (PATCHABLE_TASK_FIELDS.includes(key)) {
|
|
176
|
+
clean[key] = value;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return { ok: true, patch: clean };
|
|
180
|
+
}
|
package/lib/task-transfer.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-cron",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.24",
|
|
4
4
|
"description": "Background automation runner for DSH: isolated agent runs, script/HTTP/SSH/Docker runtimes, cost guard, notifications, heartbeats.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"test": "node --test test/*.test.mjs && node scripts/check-pack-size.mjs",
|
|
23
23
|
"build:client": "node scripts/build-client.mjs",
|
|
24
24
|
"pretest": "node scripts/build-client.mjs",
|
|
25
|
-
"preflight": "node scripts/ci-preflight.mjs"
|
|
25
|
+
"preflight": "node scripts/ci-preflight.mjs",
|
|
26
|
+
"test:engine": "node scripts/test-engines.mjs"
|
|
26
27
|
},
|
|
27
28
|
"keywords": [
|
|
28
29
|
"dsh",
|
|
@@ -51,6 +52,14 @@
|
|
|
51
52
|
"@deepseek-ai/dsh-client-locale",
|
|
52
53
|
"@deepseek-ai/dsh-client-ui-slots"
|
|
53
54
|
]
|
|
55
|
+
},
|
|
56
|
+
"compatibility": {
|
|
57
|
+
"dshReleases": {
|
|
58
|
+
"0.2.1-rc.2": "compatible",
|
|
59
|
+
"0.2.1-rc.3": "compatible",
|
|
60
|
+
"0.2.1-rc.4": "compatible"
|
|
61
|
+
},
|
|
62
|
+
"dsh": ">=0.2.1-rc.2 <0.2.2-0"
|
|
54
63
|
}
|
|
55
64
|
},
|
|
56
65
|
"dependencies": {
|