@goodandready/dsh-cron 0.2.29 → 0.2.31
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/api-import-export.js +4 -3
- package/lib/api-webhook.js +3 -2
- package/lib/api.js +10 -2
- package/lib/burn-guard.js +2 -1
- package/lib/client.js +116 -113
- package/lib/index.js +10 -7
- package/lib/integrations.js +2 -1
- package/lib/llm-actions.js +2 -1
- package/lib/logger.js +38 -0
- package/lib/models-handler.js +4 -0
- package/lib/routes.js +5 -1
- package/lib/runner-external.js +3 -2
- package/lib/runner-worktree.js +2 -1
- package/lib/runner.js +12 -11
- package/lib/scheduler-execution.js +27 -26
- package/lib/scheduler-lifecycle.js +3 -2
- package/lib/scheduler.js +5 -4
- package/lib/settings.js +3 -2
- package/lib/store.js +12 -11
- package/lib/telegram.js +4 -3
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { setLogger, logger } from './logger.js';
|
|
1
3
|
import { registerPluginUpdater } from './updater.js';
|
|
2
4
|
import { credentialRefStatus } from './secrets.js';
|
|
3
5
|
import { TaskStore } from './store.js';
|
|
@@ -110,10 +112,11 @@ export {
|
|
|
110
112
|
LEGACY_TOOL_NAMES,
|
|
111
113
|
LEGACY_ACTION_MAP,
|
|
112
114
|
};
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
const pkgUrl = new URL('../package.json', import.meta.url);
|
|
116
|
+
const pkgVersion = JSON.parse(fs.readFileSync(pkgUrl, 'utf8')).version || '0.2.29';
|
|
115
117
|
|
|
116
118
|
export function apply(ctx, rawConfig) {
|
|
119
|
+
if (ctx && ctx.logger) setLogger(ctx.logger);
|
|
117
120
|
const config = unwrapConfig(rawConfig);
|
|
118
121
|
const store = new TaskStore();
|
|
119
122
|
|
|
@@ -172,9 +175,9 @@ export function apply(ctx, rawConfig) {
|
|
|
172
175
|
try {
|
|
173
176
|
const configJobs = Array.isArray(config && config.jobs) ? config.jobs : [];
|
|
174
177
|
const summary = applyConfigSync({ store, scheduler, entries: configJobs });
|
|
175
|
-
|
|
178
|
+
logger.info('[dsh-cron] config jobs synced: ' + JSON.stringify(summary));
|
|
176
179
|
} catch (err) {
|
|
177
|
-
|
|
180
|
+
logger.error('[dsh-cron] config job sync failed:', err.message);
|
|
178
181
|
}
|
|
179
182
|
|
|
180
183
|
scheduler.start();
|
|
@@ -186,7 +189,7 @@ export function apply(ctx, rawConfig) {
|
|
|
186
189
|
const intervalMs = Number(config.heartbeatIntervalSec) * 1000;
|
|
187
190
|
heartbeatTimer = setInterval(() => {
|
|
188
191
|
fetch(config.heartbeatUrl, { method: 'GET' }).catch((err) => {
|
|
189
|
-
|
|
192
|
+
logger.warn('[dsh-cron] heartbeat ping failed:', err.message);
|
|
190
193
|
});
|
|
191
194
|
}, intervalMs);
|
|
192
195
|
}
|
|
@@ -224,7 +227,7 @@ export function apply(ctx, rawConfig) {
|
|
|
224
227
|
handler: async (req, res) => {
|
|
225
228
|
try {
|
|
226
229
|
if (req.method === 'GET') {
|
|
227
|
-
sendJson(res, 200, { ok: true, settings: store.getClientSettings() });
|
|
230
|
+
sendJson(res, 200, { ok: true, settings: { ...store.getClientSettings(), version: pkgVersion } });
|
|
228
231
|
return;
|
|
229
232
|
}
|
|
230
233
|
if (req.method === 'POST') {
|
|
@@ -236,7 +239,7 @@ export function apply(ctx, rawConfig) {
|
|
|
236
239
|
const scopeErrors = await applySettingsToScope(settingsService, payload);
|
|
237
240
|
sendJson(res, 200, {
|
|
238
241
|
ok: true,
|
|
239
|
-
settings: store.getClientSettings(),
|
|
242
|
+
settings: { ...store.getClientSettings(), version: pkgVersion },
|
|
240
243
|
scopeErrors: (scopeErrors && scopeErrors.length > 0) ? scopeErrors : undefined
|
|
241
244
|
});
|
|
242
245
|
return;
|
package/lib/integrations.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
1
2
|
/**
|
|
2
3
|
* Integration module for dsh-kanban and LLM token cost estimation.
|
|
3
4
|
*/
|
|
@@ -77,7 +78,7 @@ export async function createKanbanCard({
|
|
|
77
78
|
const data = await res.json().catch(() => ({}));
|
|
78
79
|
return { success: true, task: data.task };
|
|
79
80
|
} catch (err) {
|
|
80
|
-
|
|
81
|
+
logger.error('[dsh-cron] failed to create kanban card:', err.message);
|
|
81
82
|
return { success: false, error: err.message };
|
|
82
83
|
}
|
|
83
84
|
}
|
package/lib/llm-actions.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
1
2
|
import { bestEffort } from './best-effort.js';
|
|
2
3
|
import fs from 'node:fs';
|
|
3
4
|
|
|
@@ -103,7 +104,7 @@ export async function executeLlmActionDirectives({ directives, task, runInfo, sc
|
|
|
103
104
|
results.push({ type, status: giteaRes ? 'created' : 'skipped', details: giteaRes });
|
|
104
105
|
}
|
|
105
106
|
} catch (err) {
|
|
106
|
-
|
|
107
|
+
logger.warn(`[dsh-cron] action directive ${type} execution failed:`, err.message);
|
|
107
108
|
results.push({ type, status: 'error', error: err.message });
|
|
108
109
|
}
|
|
109
110
|
}
|
package/lib/logger.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Unified logging wrapper for dsh-cron (#204)
|
|
2
|
+
// Routes logs through ctx.logger when running inside DSH/Cordis,
|
|
3
|
+
// and safely falls back to console in tests or standalone runs.
|
|
4
|
+
|
|
5
|
+
let currentLogger = console;
|
|
6
|
+
|
|
7
|
+
export function setLogger(logger) {
|
|
8
|
+
if (logger && typeof logger === 'object') {
|
|
9
|
+
currentLogger = logger;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getLogger() {
|
|
14
|
+
return currentLogger;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const logger = {
|
|
18
|
+
info(...args) {
|
|
19
|
+
const fn = currentLogger.info || currentLogger.log || console.log;
|
|
20
|
+
return fn.call(currentLogger, ...args);
|
|
21
|
+
},
|
|
22
|
+
warn(...args) {
|
|
23
|
+
const fn = currentLogger.warn || currentLogger.log || console.warn;
|
|
24
|
+
return fn.call(currentLogger, ...args);
|
|
25
|
+
},
|
|
26
|
+
error(...args) {
|
|
27
|
+
const fn = currentLogger.error || currentLogger.log || console.error;
|
|
28
|
+
return fn.call(currentLogger, ...args);
|
|
29
|
+
},
|
|
30
|
+
debug(...args) {
|
|
31
|
+
const fn = currentLogger.debug || currentLogger.log || console.debug;
|
|
32
|
+
return fn.call(currentLogger, ...args);
|
|
33
|
+
},
|
|
34
|
+
log(...args) {
|
|
35
|
+
const fn = currentLogger.info || currentLogger.log || console.log;
|
|
36
|
+
return fn.call(currentLogger, ...args);
|
|
37
|
+
},
|
|
38
|
+
};
|
package/lib/models-handler.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export async function getModelsHandler(ctx, req, res, sendJson) {
|
|
2
|
+
if ((req.method || 'GET') !== 'GET') {
|
|
3
|
+
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
2
6
|
const current = ctx.get('agentDefaultModel')?.currentSelection?.() ?? null;
|
|
3
7
|
let providers = [];
|
|
4
8
|
try {
|
package/lib/routes.js
CHANGED
|
@@ -89,7 +89,7 @@ export function registerRoutes(ctx, { store, scheduler, getCronSettingsScope, re
|
|
|
89
89
|
path: '/dsh-cron/recipes',
|
|
90
90
|
handler: async (req, res) => {
|
|
91
91
|
try {
|
|
92
|
-
if (req.method !== 'GET') {
|
|
92
|
+
if ((req.method || 'GET') !== 'GET') {
|
|
93
93
|
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
@@ -108,6 +108,10 @@ export function registerRoutes(ctx, { store, scheduler, getCronSettingsScope, re
|
|
|
108
108
|
path: '/dsh-cron/heartbeat',
|
|
109
109
|
handler: async (req, res) => {
|
|
110
110
|
try {
|
|
111
|
+
if ((req.method || 'GET') !== 'GET') {
|
|
112
|
+
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
111
115
|
const tasks = store.list({ status: 'active' });
|
|
112
116
|
const lastRuns = tasks.map((t) => t.lastRunAt || 0);
|
|
113
117
|
sendJson(res, 200, {
|
package/lib/runner-external.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
1
2
|
import { bestEffort } from './best-effort.js';
|
|
2
3
|
import {
|
|
3
4
|
buildInvocation,
|
|
@@ -74,7 +75,7 @@ export async function runHttp(task, request, signal) {
|
|
|
74
75
|
if (!res.ok) {
|
|
75
76
|
const err = new Error(`${summary}\n${String(text).slice(0, 500)}`);
|
|
76
77
|
if (isTransientError(res.status) && attempts < maxAttempts && !signal?.aborted) {
|
|
77
|
-
|
|
78
|
+
logger.warn(`[dsh-cron] HTTP request returned ${res.status}, retrying attempt ${attempts + 1}/${maxAttempts}...`);
|
|
78
79
|
await new Promise((r) => setTimeout(r, attempts * 1000));
|
|
79
80
|
continue;
|
|
80
81
|
}
|
|
@@ -87,7 +88,7 @@ export async function runHttp(task, request, signal) {
|
|
|
87
88
|
};
|
|
88
89
|
} catch (err) {
|
|
89
90
|
if (isTransientError(err) && attempts < maxAttempts && !signal?.aborted) {
|
|
90
|
-
|
|
91
|
+
logger.warn(`[dsh-cron] HTTP request failed (${err.message}), retrying attempt ${attempts + 1}/${maxAttempts}...`);
|
|
91
92
|
await new Promise((r) => setTimeout(r, attempts * 1000));
|
|
92
93
|
continue;
|
|
93
94
|
}
|
package/lib/runner-worktree.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
1
2
|
import { bestEffort } from './best-effort.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -46,7 +47,7 @@ export async function removeTaskWorktree(cwd, wtPath) {
|
|
|
46
47
|
await new Promise((resolve) => {
|
|
47
48
|
execFile('git', ['-C', cwd, 'worktree', 'remove', wtPath], { maxBuffer: 4 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
48
49
|
if (err) {
|
|
49
|
-
|
|
50
|
+
logger.warn(`[dsh-cron] task worktree kept (not removable without force): ${wtPath} — ${String(stderr || err.message).trim()}`);
|
|
50
51
|
}
|
|
51
52
|
execFile('git', ['-C', cwd, 'worktree', 'prune'], () => resolve());
|
|
52
53
|
});
|
package/lib/runner.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
1
2
|
import { bestEffort } from './best-effort.js';
|
|
2
3
|
import { estimateTokenCost } from './integrations.js';
|
|
3
4
|
import {
|
|
@@ -141,7 +142,7 @@ export class SessionRunner {
|
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
async _run(task, signal, options = {}) {
|
|
144
|
-
|
|
145
|
+
logger.info(`[dsh-cron] executing task "${task.title}" (${task.id})`);
|
|
145
146
|
|
|
146
147
|
// 1. Non-LLM runtimes: shell, Node.js, Python, HTTP, SSH, Docker (#4 #5 #7 #8 #9)
|
|
147
148
|
const runtime = normalizeTaskType(task.type);
|
|
@@ -177,7 +178,7 @@ export class SessionRunner {
|
|
|
177
178
|
return { output: out, usage, costUsd: estimateTokenCost(task.model, usage) };
|
|
178
179
|
}
|
|
179
180
|
} catch (err) {
|
|
180
|
-
|
|
181
|
+
logger.error(`[dsh-cron] session execution failed:`, err.message);
|
|
181
182
|
throw err;
|
|
182
183
|
}
|
|
183
184
|
return this._runBare(task);
|
|
@@ -207,7 +208,7 @@ export class SessionRunner {
|
|
|
207
208
|
} catch (err) {
|
|
208
209
|
// Context window overflow auto-recovery in persistent sessions (#145)
|
|
209
210
|
if (prep.targetSessionId && isContextOverflowError(err) && !signal?.aborted) {
|
|
210
|
-
|
|
211
|
+
logger.warn(`[dsh-cron] Persistent session "${prep.targetSessionId}" exceeded context window (${err.message}). Auto-recovering with fresh session...`);
|
|
211
212
|
bestEffort('archive-rotated-session', () => {
|
|
212
213
|
const sessions = this.ctx?.get?.('sessions');
|
|
213
214
|
if (sessions && typeof sessions.archive === 'function') {
|
|
@@ -215,7 +216,7 @@ export class SessionRunner {
|
|
|
215
216
|
}
|
|
216
217
|
});
|
|
217
218
|
const rotatedSessionId = `${prep.targetSessionId}-${Date.now().toString(36)}`;
|
|
218
|
-
|
|
219
|
+
logger.info(`[dsh-cron] Rotated persistent session "${prep.targetSessionId}" -> "${rotatedSessionId}"`);
|
|
219
220
|
if (this.store) {
|
|
220
221
|
bestEffort('update-stored-rotated-session', () => {
|
|
221
222
|
const stored = this.store.get(task.id);
|
|
@@ -230,7 +231,7 @@ export class SessionRunner {
|
|
|
230
231
|
const freshPrep = await this._prepareAgentRun(effectiveTask, runtime, signal);
|
|
231
232
|
return await this._executeAgentTurn(task, freshPrep);
|
|
232
233
|
}
|
|
233
|
-
|
|
234
|
+
logger.error(`[dsh-cron] agent execution failed:`, err.message);
|
|
234
235
|
throw err;
|
|
235
236
|
}
|
|
236
237
|
}
|
|
@@ -306,7 +307,7 @@ export class SessionRunner {
|
|
|
306
307
|
taskWorktree = await this.createTaskWorktree(baseCwd, task);
|
|
307
308
|
cwd = taskWorktree;
|
|
308
309
|
} catch (wtErr) {
|
|
309
|
-
|
|
310
|
+
logger.warn('[dsh-cron] task worktree not created, running in the workspace:', wtErr.message);
|
|
310
311
|
}
|
|
311
312
|
}
|
|
312
313
|
return { baseCwd, cwd, taskWorktree };
|
|
@@ -349,7 +350,7 @@ export class SessionRunner {
|
|
|
349
350
|
await presets.standingKeyFor(preset.id);
|
|
350
351
|
}
|
|
351
352
|
} catch (presetErr) {
|
|
352
|
-
|
|
353
|
+
logger.warn('[dsh-cron] agent preset unavailable, running without one:', presetErr && presetErr.message);
|
|
353
354
|
preset = null;
|
|
354
355
|
}
|
|
355
356
|
}
|
|
@@ -375,9 +376,9 @@ export class SessionRunner {
|
|
|
375
376
|
...presetSetup,
|
|
376
377
|
});
|
|
377
378
|
isResumed = true;
|
|
378
|
-
|
|
379
|
+
logger.info(`[dsh-cron] resumed existing persistent session: ${prep.targetSessionId}`);
|
|
379
380
|
} catch (resumeErr) {
|
|
380
|
-
|
|
381
|
+
logger.info(`[dsh-cron] could not resume session ${prep.targetSessionId} (${resumeErr.message}), creating new persistent session`);
|
|
381
382
|
handle = null;
|
|
382
383
|
}
|
|
383
384
|
}
|
|
@@ -431,7 +432,7 @@ export class SessionRunner {
|
|
|
431
432
|
presets.set(handle.agent.session, task.permissionPreset);
|
|
432
433
|
}
|
|
433
434
|
} catch (ppErr) {
|
|
434
|
-
|
|
435
|
+
logger.warn('[dsh-cron] permission preset not applied:', ppErr.message);
|
|
435
436
|
}
|
|
436
437
|
}
|
|
437
438
|
|
|
@@ -462,7 +463,7 @@ export class SessionRunner {
|
|
|
462
463
|
await handle.agent.destroy();
|
|
463
464
|
}
|
|
464
465
|
} catch (disposeErr) {
|
|
465
|
-
|
|
466
|
+
logger.warn('[dsh-cron] session dispose notice:', disposeErr.message);
|
|
466
467
|
}
|
|
467
468
|
// Best-effort: keep ephemeral cron sessions out of the main chat list (#30).
|
|
468
469
|
// Persistent sessions (targetSessionId configured) stay unarchived so users can interact with them (#143).
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
1
2
|
import { checkAndApplyBurnGuard, checkTaskBudgetLimits } from './burn-guard.js';
|
|
2
3
|
import { resolveTaskForExecution } from './prompt-interpolation.js';
|
|
3
4
|
import { bestEffort } from './best-effort.js';
|
|
@@ -126,7 +127,7 @@ export async function executeWithFallback(scheduler, task, signal, options = {})
|
|
|
126
127
|
provider: task.fallbackProvider || task.provider,
|
|
127
128
|
model: task.fallbackModel,
|
|
128
129
|
};
|
|
129
|
-
|
|
130
|
+
logger.info(`[dsh-cron] task "${task.title}" failed on ${task.model || 'the default model'}, retrying once on ${task.fallbackModel}`);
|
|
130
131
|
const secondary = await scheduler.executeOnce(fallbackTask, signal, options);
|
|
131
132
|
return {
|
|
132
133
|
...secondary,
|
|
@@ -143,12 +144,12 @@ export async function executeWithFallback(scheduler, task, signal, options = {})
|
|
|
143
144
|
export function beginRun(scheduler, task, taskId) {
|
|
144
145
|
if (!scheduler.running.has(taskId) && scheduler.maxConcurrent > 0 && scheduler.running.size >= scheduler.maxConcurrent) {
|
|
145
146
|
if (task.overlapPolicy === 'queue') {
|
|
146
|
-
|
|
147
|
+
logger.info(`[dsh-cron] Task "${task.title}" (${taskId}) queued by concurrency limit (${scheduler.maxConcurrent})`);
|
|
147
148
|
scheduler.queue.push({ taskId, priority: task.priority !== undefined ? task.priority : 5, queuedAt: Date.now() });
|
|
148
149
|
scheduler.queue.sort((a, b) => (a.priority - b.priority) || (a.queuedAt - b.queuedAt));
|
|
149
150
|
return null;
|
|
150
151
|
}
|
|
151
|
-
|
|
152
|
+
logger.info(`[dsh-cron] Task "${task.title}" (${taskId}) skipped: concurrency limit (${scheduler.maxConcurrent})`);
|
|
152
153
|
scheduler.recordSkipped(taskId, `Skipped: concurrency limit reached (${scheduler.maxConcurrent} parallel runs)`);
|
|
153
154
|
return null;
|
|
154
155
|
}
|
|
@@ -157,16 +158,16 @@ export function beginRun(scheduler, task, taskId) {
|
|
|
157
158
|
const active = scheduler.running.get(taskId);
|
|
158
159
|
if (active) {
|
|
159
160
|
if (overlapPolicy === 'skip') {
|
|
160
|
-
|
|
161
|
+
logger.info(`[dsh-cron] Task "${task.title}" (${taskId}) is already running, skipping run (overlapPolicy: skip)`);
|
|
161
162
|
scheduler.recordSkipped(taskId, 'Skipped: the previous run is still in progress (overlapPolicy: skip)');
|
|
162
163
|
return null;
|
|
163
164
|
}
|
|
164
165
|
if (overlapPolicy === 'replace') {
|
|
165
|
-
|
|
166
|
+
logger.info(`[dsh-cron] Task "${task.title}" (${taskId}) is already running, canceling current run (overlapPolicy: replace)`);
|
|
166
167
|
active.controller.abort(new Error('Aborted by overlap policy: replace'));
|
|
167
168
|
}
|
|
168
169
|
if (overlapPolicy === 'queue') {
|
|
169
|
-
|
|
170
|
+
logger.info(`[dsh-cron] Task "${task.title}" (${taskId}) is already running, queueing next run (overlapPolicy: queue)`);
|
|
170
171
|
active.queueCount = (active.queueCount || 0) + 1;
|
|
171
172
|
return null;
|
|
172
173
|
}
|
|
@@ -189,7 +190,7 @@ export async function executeTask(scheduler, taskId, options = {}) {
|
|
|
189
190
|
const runs = (scheduler.store.history && scheduler.store.history.get(taskId)) || [];
|
|
190
191
|
const guard = checkTaskBudgetLimits(task, runs);
|
|
191
192
|
if (guard.exceeded) {
|
|
192
|
-
|
|
193
|
+
logger.warn(`[dsh-cron] Burn Guard pre-flight blocked task "${task.title}" (${taskId}): ${guard.reason}`);
|
|
193
194
|
scheduler.pauseTask(taskId, guard.reason);
|
|
194
195
|
scheduler.recordSkipped(taskId, `Burn Guard pre-flight pause: ${guard.reason}`);
|
|
195
196
|
checkAndApplyBurnGuard(scheduler, taskId, { status: 'error', costUsd: 0, durationMs: 0 }).catch(() => {});
|
|
@@ -222,7 +223,7 @@ export async function executeTask(scheduler, taskId, options = {}) {
|
|
|
222
223
|
|
|
223
224
|
const preflight = await executePreflight(task);
|
|
224
225
|
if (!preflight.ok) {
|
|
225
|
-
|
|
226
|
+
logger.info(`[dsh-cron] task "${task.title}" (${taskId}) skipped: ${preflight.reason}`);
|
|
226
227
|
scheduler.running.delete(taskId);
|
|
227
228
|
scheduler.recordSkipped(taskId, `Preflight check skipped: ${preflight.reason}`);
|
|
228
229
|
return null;
|
|
@@ -282,18 +283,18 @@ export async function applyModelAssists(task, runInfo, options) {
|
|
|
282
283
|
runInfo.diagnosis = inspection.diagnosis;
|
|
283
284
|
runInfo.suggestion = inspection.suggestion;
|
|
284
285
|
runInfo.confidence = inspection.confidence;
|
|
285
|
-
|
|
286
|
+
logger.info(`[dsh-cron] task "${task.title}" failure diagnosed (${inspection.confidence}): ${inspection.diagnosis}`);
|
|
286
287
|
} else if (inspection.error && task.inspectOnFailure) {
|
|
287
|
-
|
|
288
|
+
logger.warn(`[dsh-cron] failure inspection for "${task.title}" not applied: ${inspection.error}`);
|
|
288
289
|
}
|
|
289
290
|
|
|
290
291
|
const silent = await applySilentRuleToRun(task, runInfo, options);
|
|
291
292
|
if (silent.skipped) {
|
|
292
293
|
runInfo.silentSkip = true;
|
|
293
294
|
runInfo.silentReason = silent.reason;
|
|
294
|
-
|
|
295
|
+
logger.info(`[dsh-cron] task "${task.title}" stayed silent by rule: ${silent.reason || 'no reason given'}`);
|
|
295
296
|
} else if (silent.verdictError) {
|
|
296
|
-
|
|
297
|
+
logger.warn(`[dsh-cron] silent rule for "${task.title}" not applied: ${silent.verdictError}`);
|
|
297
298
|
}
|
|
298
299
|
return silent;
|
|
299
300
|
}
|
|
@@ -308,19 +309,19 @@ export async function deliverNotifications(task, runInfo, { store, resolveSecret
|
|
|
308
309
|
try {
|
|
309
310
|
settings = { ...settings, ...(await resolveSecrets(settings)) };
|
|
310
311
|
} catch (secErr) {
|
|
311
|
-
|
|
312
|
+
logger.warn('[dsh-cron] credential resolution failed:', secErr.message);
|
|
312
313
|
}
|
|
313
314
|
}
|
|
314
315
|
if (typeof deliver !== 'function') return;
|
|
315
316
|
const result = await deliver(task, runInfo, settings);
|
|
316
317
|
if (result && Array.isArray(result.delivered) && result.delivered.length) {
|
|
317
|
-
|
|
318
|
+
logger.info(`[dsh-cron] delivered "${task.title}" to: ${result.delivered.map((d) => d.channel).join(', ')}`);
|
|
318
319
|
}
|
|
319
320
|
if (result && Array.isArray(result.failures) && result.failures.length) {
|
|
320
|
-
|
|
321
|
+
logger.warn(`[dsh-cron] delivery failures for "${task.title}": ${result.failures.map((f) => `${f.channel}: ${f.error}`).join('; ')}`);
|
|
321
322
|
}
|
|
322
323
|
} catch (notifyErr) {
|
|
323
|
-
|
|
324
|
+
logger.error('[dsh-cron] notification error:', notifyErr.message);
|
|
324
325
|
}
|
|
325
326
|
}
|
|
326
327
|
|
|
@@ -343,7 +344,7 @@ export function finishRun(scheduler, task, taskId, status, queuedCount) {
|
|
|
343
344
|
}
|
|
344
345
|
}
|
|
345
346
|
} catch (err) {
|
|
346
|
-
|
|
347
|
+
logger.error('[dsh-cron] failed to persist the next run of task', taskId + ':', err.message);
|
|
347
348
|
}
|
|
348
349
|
|
|
349
350
|
try {
|
|
@@ -359,7 +360,7 @@ export function finishRun(scheduler, task, taskId, status, queuedCount) {
|
|
|
359
360
|
scheduler.retryTimers.delete(taskId);
|
|
360
361
|
scheduler.runTask(taskId, { isRetry: true });
|
|
361
362
|
}, backoffMs));
|
|
362
|
-
|
|
363
|
+
logger.info(`[dsh-cron] retry ${attempt}/${maxRetries} for task "${task.title}" in ${backoffMs}ms`);
|
|
363
364
|
} else if (!failed && task.attempts) {
|
|
364
365
|
task.attempts = 0;
|
|
365
366
|
scheduler.store.set(task);
|
|
@@ -368,7 +369,7 @@ export function finishRun(scheduler, task, taskId, status, queuedCount) {
|
|
|
368
369
|
scheduler.store.set(task);
|
|
369
370
|
}
|
|
370
371
|
} catch (err) {
|
|
371
|
-
|
|
372
|
+
logger.error('[dsh-cron] retry bookkeeping failed for task', taskId + ':', err.message);
|
|
372
373
|
}
|
|
373
374
|
|
|
374
375
|
if (queuedCount > 0) {
|
|
@@ -398,7 +399,7 @@ export async function handleCompleteRun(scheduler, task, taskId, currentRun, out
|
|
|
398
399
|
};
|
|
399
400
|
|
|
400
401
|
if (isFailure && task.selfHealingCommand) {
|
|
401
|
-
|
|
402
|
+
logger.info(`[dsh-cron] running self-healing command for task "${task.title}": ${task.selfHealingCommand}`);
|
|
402
403
|
try {
|
|
403
404
|
await new Promise((resolve) => {
|
|
404
405
|
exec(task.selfHealingCommand, { timeout: 30000 }, (shErr, stdout, stderr) => {
|
|
@@ -435,7 +436,7 @@ export async function handleCompleteRun(scheduler, task, taskId, currentRun, out
|
|
|
435
436
|
try {
|
|
436
437
|
scheduler.store.recordRun(taskId, runInfo);
|
|
437
438
|
} catch (recordErr) {
|
|
438
|
-
|
|
439
|
+
logger.error(`[dsh-cron] failed to record the run of task ${taskId}:`, recordErr.message);
|
|
439
440
|
}
|
|
440
441
|
|
|
441
442
|
const finishedRun = scheduler.running.get(taskId);
|
|
@@ -480,7 +481,7 @@ export async function handleCompleteRun(scheduler, task, taskId, currentRun, out
|
|
|
480
481
|
}
|
|
481
482
|
}
|
|
482
483
|
} catch (actErr) {
|
|
483
|
-
|
|
484
|
+
logger.warn(`[dsh-cron] structured action error for ${taskId}:`, actErr.message);
|
|
484
485
|
}
|
|
485
486
|
}
|
|
486
487
|
|
|
@@ -492,7 +493,7 @@ export async function handleCompleteRun(scheduler, task, taskId, currentRun, out
|
|
|
492
493
|
const targetId = nextTaskId.trim();
|
|
493
494
|
const nextTask = scheduler.store.get(targetId);
|
|
494
495
|
if (nextTask) {
|
|
495
|
-
|
|
496
|
+
logger.info(`[dsh-cron] triggering chained task "${nextTask.title}" (${targetId}) from "${task.title}" (depth ${currentDepth + 1})`);
|
|
496
497
|
scheduler.runNow(targetId, {
|
|
497
498
|
chainDepth: currentDepth + 1,
|
|
498
499
|
prevOutput: outcome.output || outcome.error || '',
|
|
@@ -501,13 +502,13 @@ export async function handleCompleteRun(scheduler, task, taskId, currentRun, out
|
|
|
501
502
|
prevDurationMs: Date.now() - start,
|
|
502
503
|
prevCostUsd: outcome.costUsd || 0,
|
|
503
504
|
}).catch((err) => {
|
|
504
|
-
|
|
505
|
+
logger.warn(`[dsh-cron] chained task ${targetId} failed to trigger:`, err.message);
|
|
505
506
|
});
|
|
506
507
|
} else {
|
|
507
|
-
|
|
508
|
+
logger.warn(`[dsh-cron] chained task target not found: ${targetId}`);
|
|
508
509
|
}
|
|
509
510
|
} else {
|
|
510
|
-
|
|
511
|
+
logger.warn(`[dsh-cron] chained task depth limit (5) reached for task ${task.id} -> ${nextTaskId}. Loop prevented.`);
|
|
511
512
|
}
|
|
512
513
|
}
|
|
513
514
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
1
2
|
import { bestEffort } from './best-effort.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -41,7 +42,7 @@ export async function checkHeartbeats(scheduler) {
|
|
|
41
42
|
task.updatedAt = now;
|
|
42
43
|
scheduler.store.save();
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
logger.warn(`[dsh-cron] Heartbeat missed for "${task.title}" (${task.id})!`);
|
|
45
46
|
const runInfo = {
|
|
46
47
|
at: now,
|
|
47
48
|
status: 'missed',
|
|
@@ -62,7 +63,7 @@ export async function checkHeartbeats(scheduler) {
|
|
|
62
63
|
chainDepth: 1,
|
|
63
64
|
prevOutput: runInfo.output,
|
|
64
65
|
prevTaskId: task.id,
|
|
65
|
-
}).catch((err) =>
|
|
66
|
+
}).catch((err) => logger.warn('[dsh-cron] heartbeat onFailure trigger error:', err.message));
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
}
|
package/lib/scheduler.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
1
2
|
import { bestEffort } from './best-effort.js';
|
|
2
3
|
import { Cron } from 'croner';
|
|
3
4
|
import {
|
|
@@ -297,7 +298,7 @@ export class TaskScheduler {
|
|
|
297
298
|
if (missed) {
|
|
298
299
|
const missedAt = task.nextRunAt;
|
|
299
300
|
const policy = task.misfirePolicy || 'skip';
|
|
300
|
-
|
|
301
|
+
logger.warn(`[dsh-cron] Task "${task.title}" (${task.id}) missed scheduled run at ${new Date(missedAt).toISOString()} (misfire: ${policy})`);
|
|
301
302
|
this.countRun('missed');
|
|
302
303
|
this.store.recordRun(task.id, {
|
|
303
304
|
at: missedAt,
|
|
@@ -369,7 +370,7 @@ export class TaskScheduler {
|
|
|
369
370
|
const timer = setTimeout(() => {
|
|
370
371
|
this.timers.delete(task.id);
|
|
371
372
|
this.runTask(task.id).catch((err) => {
|
|
372
|
-
|
|
373
|
+
logger.error(`[dsh-cron] one-shot run of task ${task.id} failed:`, (err && err.message) || err);
|
|
373
374
|
});
|
|
374
375
|
}, delay);
|
|
375
376
|
this.timers.set(task.id, timer);
|
|
@@ -380,7 +381,7 @@ export class TaskScheduler {
|
|
|
380
381
|
const job = new Cron(parsed.cronPattern, {
|
|
381
382
|
protect: true,
|
|
382
383
|
sloppyRanges: true,
|
|
383
|
-
catch: (err) =>
|
|
384
|
+
catch: (err) => logger.error(`[dsh-cron] scheduled run of "${task.title}" (${task.id}) failed:`, (err && err.message) || err),
|
|
384
385
|
...(timezone ? { timezone } : {}),
|
|
385
386
|
}, async () => {
|
|
386
387
|
await this.runTask(task.id);
|
|
@@ -407,7 +408,7 @@ export class TaskScheduler {
|
|
|
407
408
|
}
|
|
408
409
|
this.scheduleCron(task, parsed);
|
|
409
410
|
} catch (err) {
|
|
410
|
-
|
|
411
|
+
logger.error(`[dsh-cron] failed to schedule task ${task.id}:`, err.message);
|
|
411
412
|
}
|
|
412
413
|
}
|
|
413
414
|
|
package/lib/settings.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
1
2
|
import z from '@deepseek-ai/schemastery';
|
|
2
3
|
import { credentialRefStatus } from './secrets.js';
|
|
3
4
|
import { TaskStore } from './store.js';
|
|
@@ -147,14 +148,14 @@ export function sanitizeSettingsPayload(body) {
|
|
|
147
148
|
if (key in payload && typeof payload[key] === 'string' && payload[key].trim()) {
|
|
148
149
|
const refCheck = credentialRefStatus(payload[key]);
|
|
149
150
|
if (!refCheck.ok) {
|
|
150
|
-
|
|
151
|
+
logger.warn(`[dsh-cron] invalid credential reference in "${key}": ${refCheck.error}`);
|
|
151
152
|
delete payload[key];
|
|
152
153
|
}
|
|
153
154
|
}
|
|
154
155
|
}
|
|
155
156
|
for (const key of TaskStore.FORBIDDEN_SETTING_KEYS) {
|
|
156
157
|
if (key in payload) {
|
|
157
|
-
|
|
158
|
+
logger.warn(`[dsh-cron] refusing raw secret "${key}" from the client — use a credential reference`);
|
|
158
159
|
delete payload[key];
|
|
159
160
|
}
|
|
160
161
|
}
|
package/lib/store.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
1
2
|
import { bestEffort } from './best-effort.js';
|
|
2
3
|
import fs from 'node:fs';
|
|
3
4
|
import path from 'node:path';
|
|
@@ -74,21 +75,21 @@ export class TaskStore {
|
|
|
74
75
|
bestEffort('backup-initial', () => fs.copyFileSync(this.filePath, bakPath));
|
|
75
76
|
}
|
|
76
77
|
} catch (err) {
|
|
77
|
-
|
|
78
|
+
logger.error('[dsh-cron] store read error (corrupted file):', err.message);
|
|
78
79
|
try {
|
|
79
80
|
const corruptedPath = `${this.filePath}.corrupted.${Date.now()}`;
|
|
80
81
|
fs.copyFileSync(this.filePath, corruptedPath);
|
|
81
|
-
|
|
82
|
+
logger.warn(`[dsh-cron] corrupted store backed up to ${corruptedPath}`);
|
|
82
83
|
} catch (snapErr) {
|
|
83
|
-
|
|
84
|
+
logger.error('[dsh-cron] failed to snapshot corrupted store:', snapErr.message);
|
|
84
85
|
}
|
|
85
86
|
if (fs.existsSync(bakPath)) {
|
|
86
87
|
try {
|
|
87
88
|
const rawBak = fs.readFileSync(bakPath, 'utf-8');
|
|
88
89
|
loadData(rawBak);
|
|
89
|
-
|
|
90
|
+
logger.warn(`[dsh-cron] successfully recovered tasks store from ${bakPath}`);
|
|
90
91
|
} catch (bakErr) {
|
|
91
|
-
|
|
92
|
+
logger.error('[dsh-cron] failed to recover tasks store from .bak:', bakErr.message);
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
95
|
}
|
|
@@ -97,9 +98,9 @@ export class TaskStore {
|
|
|
97
98
|
const rawBak = fs.readFileSync(bakPath, 'utf-8');
|
|
98
99
|
loadData(rawBak);
|
|
99
100
|
bestEffort('restore-from-bak', () => fs.copyFileSync(bakPath, this.filePath));
|
|
100
|
-
|
|
101
|
+
logger.warn(`[dsh-cron] restored missing tasks store from ${bakPath}`);
|
|
101
102
|
} catch (bakErr) {
|
|
102
|
-
|
|
103
|
+
logger.error('[dsh-cron] failed to restore tasks store from .bak:', bakErr.message);
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
106
|
}
|
|
@@ -158,7 +159,7 @@ export class TaskStore {
|
|
|
158
159
|
bestEffort('backup-on-save', () => fs.copyFileSync(this.filePath, `${this.filePath}.bak`));
|
|
159
160
|
}
|
|
160
161
|
} catch (err) {
|
|
161
|
-
|
|
162
|
+
logger.error('[dsh-cron] store save error:', err.message);
|
|
162
163
|
}
|
|
163
164
|
}
|
|
164
165
|
|
|
@@ -222,7 +223,7 @@ export class TaskStore {
|
|
|
222
223
|
|
|
223
224
|
for (const key of TaskStore.FORBIDDEN_SETTING_KEYS) {
|
|
224
225
|
if (key in patch) {
|
|
225
|
-
|
|
226
|
+
logger.warn(`[dsh-cron] refusing to store raw secret "${key}" in settings — use a credential reference instead`);
|
|
226
227
|
delete patch[key];
|
|
227
228
|
}
|
|
228
229
|
}
|
|
@@ -441,7 +442,7 @@ export class TaskStore {
|
|
|
441
442
|
fs.writeFileSync(tmp, JSON.stringify(archive), 'utf-8');
|
|
442
443
|
fs.renameSync(tmp, archivePath);
|
|
443
444
|
} catch (archiveErr) {
|
|
444
|
-
|
|
445
|
+
logger.error('[dsh-cron] history archive error:', archiveErr.message);
|
|
445
446
|
}
|
|
446
447
|
}
|
|
447
448
|
|
|
@@ -456,7 +457,7 @@ export class TaskStore {
|
|
|
456
457
|
const archivePath = path.join(dir, 'tasks-history-archive.json');
|
|
457
458
|
if (!fs.existsSync(archivePath)) return { runs: [], total: 0 };
|
|
458
459
|
const archive = JSON.parse(fs.readFileSync(archivePath, 'utf-8')) || {};
|
|
459
|
-
let runs = Array.isArray(archive[id]) ? archive[id] : [];
|
|
460
|
+
let runs = Array.isArray(archive[id]) ? [...archive[id]].sort((a, b) => ((b.at || b.timestamp || b.startedAt || 0) - (a.at || a.timestamp || a.startedAt || 0))) : [];
|
|
460
461
|
if (search && search.trim()) {
|
|
461
462
|
const q = search.toLowerCase();
|
|
462
463
|
runs = runs.filter(r => (r.output && r.output.toLowerCase().includes(q)) || (r.error && r.error.toLowerCase().includes(q)));
|