@goodandready/dsh-cron 0.3.24 → 0.3.26

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.
Files changed (84) hide show
  1. package/lib/api-actions.js +1 -317
  2. package/lib/api-collection.js +1 -250
  3. package/lib/api-helpers.js +1 -100
  4. package/lib/api-import-export.js +1 -155
  5. package/lib/api-item.js +1 -190
  6. package/lib/api-task-webhook.js +1 -247
  7. package/lib/api-webhook.js +1 -183
  8. package/lib/api.js +1 -178
  9. package/lib/artifacts.js +1 -200
  10. package/lib/chain-checkpoints.js +1 -223
  11. package/lib/client-src/00-open.js +62 -0
  12. package/lib/client-src/10-locales.js +484 -0
  13. package/lib/client-src/20-styles.js +64 -0
  14. package/lib/client-src/30-utils.js +248 -0
  15. package/lib/client-src/40-delivery-settings.js +96 -0
  16. package/lib/client-src/45-dag-graph-view.js +287 -0
  17. package/lib/client-src/46-live-logs-modal.js +108 -0
  18. package/lib/client-src/47-analytics-digest-modal.js +166 -0
  19. package/lib/client-src/48-timeline-heatmap-view.js +176 -0
  20. package/lib/client-src/50-cron-screen-state.js +447 -0
  21. package/lib/client-src/52-cron-screen-actions.js +330 -0
  22. package/lib/client-src/53-cron-screen-modal-actions.js +640 -0
  23. package/lib/client-src/54-cron-screen-analytics-actions.js +68 -0
  24. package/lib/client-src/55-cron-screen-view.js +378 -0
  25. package/lib/client-src/60-modal-task-form-head.js +580 -0
  26. package/lib/client-src/62-modal-task-form-tail.js +466 -0
  27. package/lib/client-src/65-modal-dialogs.js +350 -0
  28. package/lib/client-src/70-settings-card.js +420 -0
  29. package/lib/client-src/80-sidebar-jobs.js +200 -0
  30. package/lib/client-src/90-close.js +200 -0
  31. package/lib/client.js +48 -3
  32. package/lib/commands.js +200 -0
  33. package/lib/durable-journal.js +1 -188
  34. package/lib/external-api.js +1 -74
  35. package/lib/index.js +17 -0
  36. package/lib/log-streamer.js +10 -2
  37. package/lib/metrics.js +1 -125
  38. package/lib/routes/api-actions.js +317 -0
  39. package/lib/routes/api-collection.js +250 -0
  40. package/lib/routes/api-helpers.js +100 -0
  41. package/lib/routes/api-import-export.js +155 -0
  42. package/lib/routes/api-item.js +191 -0
  43. package/lib/routes/api-task-webhook.js +247 -0
  44. package/lib/routes/api-webhook.js +183 -0
  45. package/lib/routes/api.js +178 -0
  46. package/lib/routes/external-api.js +74 -0
  47. package/lib/routes/index.js +11 -0
  48. package/lib/routes/metrics.js +125 -0
  49. package/lib/routes/routes.js +549 -0
  50. package/lib/routes.js +1 -549
  51. package/lib/runner-external.js +1 -352
  52. package/lib/runner-worktree.js +1 -90
  53. package/lib/runner.js +1 -881
  54. package/lib/runtimes/index.js +4 -0
  55. package/lib/runtimes/runner-external.js +352 -0
  56. package/lib/runtimes/runner-worktree.js +90 -0
  57. package/lib/runtimes/runner.js +881 -0
  58. package/lib/runtimes/runtimes.js +198 -0
  59. package/lib/runtimes.js +1 -198
  60. package/lib/scheduler/index.js +6 -0
  61. package/lib/scheduler/scheduler-assists.js +142 -0
  62. package/lib/scheduler/scheduler-completion.js +417 -0
  63. package/lib/scheduler/scheduler-execution.js +360 -0
  64. package/lib/scheduler/scheduler-lifecycle.js +157 -0
  65. package/lib/scheduler/scheduler-preflight.js +195 -0
  66. package/lib/scheduler/scheduler.js +751 -0
  67. package/lib/scheduler-assists.js +1 -142
  68. package/lib/scheduler-completion.js +1 -417
  69. package/lib/scheduler-execution.js +1 -360
  70. package/lib/scheduler-lifecycle.js +1 -157
  71. package/lib/scheduler-preflight.js +1 -195
  72. package/lib/scheduler.js +1 -751
  73. package/lib/store/artifacts.js +200 -0
  74. package/lib/store/chain-checkpoints.js +223 -0
  75. package/lib/store/durable-journal.js +188 -0
  76. package/lib/store/index.js +6 -0
  77. package/lib/store/store.js +1179 -0
  78. package/lib/store/task-schema.js +180 -0
  79. package/lib/store/task-transfer.js +310 -0
  80. package/lib/store.js +2 -1179
  81. package/lib/task-schema.js +1 -180
  82. package/lib/task-transfer.js +1 -310
  83. package/lib/tui-settings.js +157 -0
  84. package/package.json +6 -3
@@ -1,360 +1 @@
1
- import { recordPromptRunOutcome } from './prompt-evolver.js';
2
- import { evaluateAdaptiveRun, calculateNextAdaptiveState } from './adaptive.js';
3
- import { globalBudgetManager } from './global-budgets.js';
4
- import { logger } from './logger.js';
5
- import { checkAndApplyBurnGuard, checkTaskBudgetLimits } from './burn-guard.js';
6
- import { resolveTaskForExecution } from './prompt-interpolation.js';
7
- import { approvalManager } from './approval.js';
8
- import {
9
- AGENT_TASK_TYPES,
10
- addUsage,
11
- executePreflight,
12
- shouldUseFallback,
13
- executeOnce,
14
- executeWithFallback,
15
- } from './scheduler-preflight.js';
16
- import {
17
- inspectFailureRun,
18
- applySilentRuleToRun,
19
- applyModelAssists,
20
- deliverNotifications,
21
- } from './scheduler-assists.js';
22
- import { handleCompleteRun } from './scheduler-completion.js';
23
-
24
- export {
25
- AGENT_TASK_TYPES,
26
- addUsage,
27
- executePreflight,
28
- shouldUseFallback,
29
- executeOnce,
30
- executeWithFallback,
31
- inspectFailureRun,
32
- applySilentRuleToRun,
33
- applyModelAssists,
34
- deliverNotifications,
35
- handleCompleteRun,
36
- };
37
-
38
- /**
39
- * Begin a task run with concurrency and overlap policy checks.
40
- */
41
- export function beginRun(scheduler, task, taskId, options = {}) {
42
- if (scheduler.isStopped) return null;
43
- if (!scheduler.running.has(taskId) && scheduler.maxConcurrent > 0 && scheduler.running.size >= scheduler.maxConcurrent) {
44
- if (task.overlapPolicy === 'queue') {
45
- logger.info(`[dsh-cron] Task "${task.title}" (${taskId}) queued by concurrency limit (${scheduler.maxConcurrent})`);
46
- scheduler.queue.push({
47
- taskId,
48
- options: { ...options },
49
- priority: task.priority !== undefined ? task.priority : 5,
50
- queuedAt: Date.now(),
51
- });
52
- scheduler.queue.sort((a, b) => (a.priority - b.priority) || (a.queuedAt - b.queuedAt));
53
- return null;
54
- }
55
- logger.info(`[dsh-cron] Task "${task.title}" (${taskId}) skipped: concurrency limit (${scheduler.maxConcurrent})`);
56
- scheduler.recordSkipped(taskId, `Skipped: concurrency limit reached (${scheduler.maxConcurrent} parallel runs)`);
57
- return null;
58
- }
59
-
60
- const group = task.concurrencyGroup && task.concurrencyGroup !== 'default' ? String(task.concurrencyGroup).trim() : null;
61
- const groupLimit = scheduler.getGroupLimit ? scheduler.getGroupLimit(group) : (group ? 1 : 0);
62
- if (group && groupLimit > 0 && !scheduler.running.has(taskId)) {
63
- const runningInGroup = scheduler.getRunningGroupCount ? scheduler.getRunningGroupCount(group) : 0;
64
- if (runningInGroup >= groupLimit) {
65
- if (task.overlapPolicy === 'queue') {
66
- logger.info(`[dsh-cron] Task "${task.title}" (${taskId}) queued by group "${group}" limit (${groupLimit})`);
67
- scheduler.queue.push({
68
- taskId,
69
- options: { ...options },
70
- priority: task.priority !== undefined ? task.priority : 5,
71
- queuedAt: Date.now(),
72
- });
73
- scheduler.queue.sort((a, b) => (a.priority - b.priority) || (a.queuedAt - b.queuedAt));
74
- return null;
75
- }
76
- logger.info(`[dsh-cron] Task "${task.title}" (${taskId}) skipped: concurrency group "${group}" limit reached (${groupLimit})`);
77
- scheduler.recordSkipped(taskId, `Skipped: concurrency group "${group}" limit reached (${groupLimit} parallel runs)`);
78
- return null;
79
- }
80
- }
81
-
82
- const overlapPolicy = task.overlapPolicy || 'skip';
83
- const active = scheduler.running.get(taskId);
84
- if (active) {
85
- if (overlapPolicy === 'skip') {
86
- logger.info(`[dsh-cron] Task "${task.title}" (${taskId}) is already running, skipping run (overlapPolicy: skip)`);
87
- scheduler.recordSkipped(taskId, 'Skipped: the previous run is still in progress (overlapPolicy: skip)');
88
- return null;
89
- }
90
- if (overlapPolicy === 'replace') {
91
- logger.info(`[dsh-cron] Task "${task.title}" (${taskId}) is already running, canceling current run (overlapPolicy: replace)`);
92
- active.controller.abort(new Error('Aborted by overlap policy: replace'));
93
- }
94
- if (overlapPolicy === 'queue') {
95
- logger.info(`[dsh-cron] Task "${task.title}" (${taskId}) is already running, queueing next run (overlapPolicy: queue)`);
96
- active.queuedRuns = active.queuedRuns || [];
97
- active.queuedRuns.push({ options: { ...options }, queuedAt: Date.now() });
98
- active.queueCount = active.queuedRuns.length;
99
- return null;
100
- }
101
- }
102
-
103
- const controller = new AbortController();
104
- const runId = 'run_' + Math.random().toString(36).slice(2, 9) + '_' + Date.now();
105
- const pendingOverlap = Array.isArray(options?.pendingOverlapQueue) ? options.pendingOverlapQueue : [];
106
- const currentRun = {
107
- id: runId,
108
- controller,
109
- startedAt: Date.now(),
110
- queuedRuns: pendingOverlap,
111
- queueCount: pendingOverlap.length,
112
- concurrencyGroup: group || 'default',
113
- };
114
- scheduler.running.set(taskId, currentRun);
115
- return currentRun;
116
- }
117
-
118
- /**
119
- * Run task by ID.
120
- */
121
- export async function executeTask(scheduler, taskId, options = {}) {
122
- if (scheduler.isStopped) return null;
123
- const task = scheduler.store.get(taskId);
124
- if (!task) return null;
125
-
126
- // Dry-run path: read-only evaluation without mutating store, history, attempts, or triggering alerts (#229)
127
- if (options.dryRun) {
128
- const runs = (scheduler.store.history && scheduler.store.history.get(taskId)) || [];
129
- const guard = checkTaskBudgetLimits(task, runs);
130
- if (guard.exceeded) {
131
- return {
132
- ok: false,
133
- dryRun: true,
134
- status: 'error',
135
- error: guard.reason,
136
- preflightBlocked: true,
137
- };
138
- }
139
- const preflight = await executePreflight(task);
140
- if (!preflight.ok) {
141
- return {
142
- ok: false,
143
- dryRun: true,
144
- status: 'skipped',
145
- error: `Preflight check skipped: ${preflight.reason}`,
146
- preflightBlocked: true,
147
- };
148
- }
149
- const start = Date.now();
150
- const outcome = await scheduler.executeWithFallback(
151
- task,
152
- AbortSignal.timeout(Math.min((task.timeoutSeconds || 1800) * 1000, 30000)),
153
- options
154
- );
155
- return {
156
- ok: outcome.status !== 'error' && outcome.status !== 'timeout',
157
- dryRun: true,
158
- status: outcome.status,
159
- output: outcome.output,
160
- error: outcome.error,
161
- durationMs: Date.now() - start,
162
- usage: outcome.usage,
163
- costUsd: outcome.costUsd,
164
- model: outcome.model || task.model || '',
165
- };
166
- }
167
- // 0. Idempotency Check (#317): deduplicate runs with an already completed idempotency key
168
- if (options.idempotencyKey && scheduler.store.durableJournal) {
169
- const existing = scheduler.store.durableJournal.isIdempotencyKeyExecuted(taskId, options.idempotencyKey);
170
- if (existing) {
171
- logger.info(`[dsh-cron] Idempotency key "${options.idempotencyKey}" already executed for task "${taskId}", returning cached run`);
172
- return {
173
- ok: existing.status === 'completed' || existing.status === 'success',
174
- idempotentReplay: true,
175
- runId: existing.id,
176
- status: existing.status,
177
- output: existing.output,
178
- costUsd: existing.costUsd,
179
- durationMs: existing.durationMs,
180
- };
181
- }
182
- }
183
-
184
-
185
- // 1. Approval Gate (#318): Human-in-the-loop check before executing actions requiring approval
186
- if (task.requireApproval && !options.approvedBy) {
187
- let pending = approvalManager.getPendingProposalForTask(taskId);
188
- if (!pending) {
189
- pending = approvalManager.createProposal(taskId, null, {
190
- actionType: 'run',
191
- summary: `Execution of task "${task.title}" requires human approval`,
192
- details: { prompt: task.prompt, type: task.type, model: task.model }
193
- });
194
- }
195
- logger.info(`[dsh-cron] Task "${task.title}" (${taskId}) paused awaiting human approval (${pending.id})`);
196
- scheduler.recordSkipped(taskId, `Awaiting human approval: proposal ${pending.id}`);
197
- return { ok: false, status: 'waiting_approval', proposalId: pending.id, stoppedReason: 'Awaiting human approval' };
198
- }
199
-
200
- // 2. Global & Provider/Model Budgets (#320)
201
- const settings = typeof scheduler.store.getSettings === 'function' ? scheduler.store.getSettings() : {};
202
- const globalBudget = globalBudgetManager.checkBudget(scheduler.store, task, settings);
203
- if (globalBudget.exceeded) {
204
- logger.warn(`[dsh-cron] Global budget limit blocked task "${task.title}" (${taskId}): ${globalBudget.reason}`);
205
- scheduler.recordSkipped(taskId, `Budget limit reached: ${globalBudget.reason}`);
206
- return { ok: false, status: 'error', error: globalBudget.reason, budgetBlocked: true };
207
- }
208
-
209
- // Pre-flight Burn Guard check (#181): prevent executing task if budget already breached
210
- const runs = (scheduler.store.history && scheduler.store.history.get(taskId)) || [];
211
- const guard = checkTaskBudgetLimits(task, runs);
212
- if (guard.exceeded) {
213
- logger.warn(`[dsh-cron] Burn Guard pre-flight blocked task "${task.title}" (${taskId}): ${guard.reason}`);
214
- scheduler.pauseTask(taskId, guard.reason);
215
- scheduler.recordSkipped(taskId, `Burn Guard pre-flight pause: ${guard.reason}`);
216
- checkAndApplyBurnGuard(scheduler, taskId, { status: 'error', costUsd: 0, durationMs: 0 }).catch(() => {});
217
- return { ok: false, status: 'error', error: guard.reason, preflightBlocked: true };
218
- }
219
-
220
- if (!options.isRetry && task.attempts) {
221
- task.attempts = 0;
222
- scheduler.store.set(task);
223
- }
224
-
225
- const currentRun = scheduler.beginRun(task, taskId, options);
226
- if (currentRun === null) return null;
227
-
228
- const preflightStart = Date.now();
229
- const preflight = await executePreflight(task);
230
- const preflightDurationMs = Date.now() - preflightStart;
231
- if (!preflight.ok) {
232
- logger.info(`[dsh-cron] task "${task.title}" (${taskId}) skipped: ${preflight.reason}`);
233
- scheduler.running.delete(taskId);
234
- scheduler.recordSkipped(taskId, `Preflight check skipped: ${preflight.reason}`);
235
- return null;
236
- }
237
-
238
- const start = Date.now();
239
- const reservationId = globalBudgetManager.reserve(task);
240
- let outcome;
241
- try {
242
- outcome = await scheduler.executeWithFallback(task, currentRun.controller.signal, options);
243
- } finally {
244
- globalBudgetManager.settle(reservationId);
245
- }
246
- await scheduler.completeRun(task, taskId, currentRun, outcome, start, {
247
- ...options,
248
- preflightDurationMs,
249
- });
250
- return outcome;
251
- }
252
-
253
- /**
254
- * Post-run bookkeeping: one-shot completion, next-run timestamp, retry backoff, draining.
255
- */
256
- export function finishRun(scheduler, taskSnapshot, taskId, status, queuedCount, meta = {}) {
257
- scheduler.countRun(status);
258
-
259
- // Track prompt version success rate outcome (#331)
260
- if (taskSnapshot && taskSnapshot.prompt && (taskSnapshot.promptVersion || taskSnapshot.promptHistory)) {
261
- const isSuccess = status === 'success';
262
- recordPromptRunOutcome(taskSnapshot, isSuccess);
263
- if (scheduler.store && typeof scheduler.store.set === 'function') {
264
- scheduler.store.set(taskSnapshot);
265
- }
266
- }
267
-
268
- // Adaptive polling backoff (#329)
269
- if (taskSnapshot && taskSnapshot.adaptive && taskSnapshot.adaptive.enabled && !taskSnapshot.oneShot && taskSnapshot.status === 'active' && !scheduler.isStopped) {
270
- const history = scheduler.store.getHistory ? scheduler.store.getHistory(taskId, 2) : [];
271
- const prevRun = history.length > 1 ? history[1] : null;
272
- const outcomeForEval = meta?.outcome || { status };
273
- const runInfoForEval = meta?.runInfo || {};
274
- const evalRes = evaluateAdaptiveRun(taskSnapshot, outcomeForEval, runInfoForEval, prevRun);
275
- const nextAdaptiveState = calculateNextAdaptiveState(taskSnapshot, evalRes.isIdle);
276
- taskSnapshot.adaptiveState = nextAdaptiveState;
277
- scheduler.store.set(taskSnapshot);
278
-
279
- if (nextAdaptiveState.backedOff) {
280
- logger.info(`[dsh-cron] adaptive backoff active for "${taskSnapshot.title}" (${taskSnapshot.id}): interval scaled to ${nextAdaptiveState.effectiveIntervalStr} (idle count: ${nextAdaptiveState.idleCount})`);
281
- scheduler.armAdaptiveTimer(taskId, nextAdaptiveState.currentIntervalMs);
282
- } else {
283
- scheduler.clearAdaptiveTimer(taskId);
284
- if (!scheduler.jobs.has(taskId)) {
285
- scheduler.scheduleTask(taskSnapshot);
286
- }
287
- }
288
- }
289
-
290
- // If this run was superseded by a newer run or scheduler is stopped, skip updating task
291
- if (meta.isCurrentRun === false || scheduler.isStopped) {
292
- return;
293
- }
294
-
295
- // Refetch live task from store to avoid resurrecting deleted task or overwriting edits (#215)
296
- const current = scheduler.store.get(taskId);
297
- if (!current) {
298
- logger.info(`[dsh-cron] finishRun: task "${taskId}" no longer exists in store; skipping completion write`);
299
- return;
300
- }
301
-
302
- try {
303
- if (current.oneShot) {
304
- current.status = 'completed';
305
- current.nextRunAt = null;
306
- scheduler.store.set(current);
307
- } else {
308
- const job = scheduler.jobs.get(taskId);
309
- if (job) {
310
- const next = job.nextRun();
311
- current.nextRunAt = next ? next.getTime() : null;
312
- scheduler.store.set(current);
313
- }
314
- }
315
- } catch (err) {
316
- logger.error('[dsh-cron] failed to persist the next run of task', taskId + ':', err.message);
317
- }
318
-
319
- try {
320
- const failed = status === 'error' || status === 'timeout';
321
- const maxRetries = Number(current.maxRetries) > 0 ? Number(current.maxRetries) : 0;
322
- if (failed && maxRetries > 0 && (current.attempts || 0) < maxRetries && current.status !== 'paused' && !scheduler.isStopped) {
323
- current.attempts = (current.attempts || 0) + 1;
324
- const backoffMs = (Number(current.retryBackoffMs) > 0 ? Number(current.retryBackoffMs) : 30000) * Math.pow(2, current.attempts - 1);
325
- const attempt = current.attempts;
326
- scheduler.store.set(current);
327
- scheduler.clearRetryTimer(taskId);
328
- scheduler.retryTimers.set(taskId, setTimeout(() => {
329
- scheduler.retryTimers.delete(taskId);
330
- if (!scheduler.isStopped) {
331
- scheduler.runTask(taskId, { isRetry: true });
332
- }
333
- }, backoffMs));
334
- logger.info(`[dsh-cron] retry ${attempt}/${maxRetries} for task "${current.title}" in ${backoffMs}ms`);
335
- } else if (!failed && current.attempts) {
336
- current.attempts = 0;
337
- scheduler.store.set(current);
338
- } else if (failed && maxRetries > 0 && (current.attempts || 0) >= maxRetries) {
339
- current.attempts = 0;
340
- scheduler.store.set(current);
341
- }
342
- } catch (err) {
343
- logger.error('[dsh-cron] retry bookkeeping failed for task', taskId + ':', err.message);
344
- }
345
-
346
- if (queuedCount > 0 && !scheduler.isStopped) {
347
- const queuedRuns = Array.isArray(meta?.queuedRuns) ? [...meta.queuedRuns] : [];
348
- const nextItem = queuedRuns.shift();
349
- const nextOptions = {
350
- ...(nextItem?.options || {}),
351
- ...(queuedRuns.length > 0 ? { pendingOverlapQueue: queuedRuns } : {}),
352
- };
353
- setImmediate(() => {
354
- if (!scheduler.isStopped) {
355
- scheduler.runTask(taskId, nextOptions);
356
- }
357
- });
358
- }
359
- }
360
-
1
+ export * from './scheduler/scheduler-execution.js';
@@ -1,157 +1 @@
1
- import { logger } from './logger.js';
2
- import { bestEffort } from './best-effort.js';
3
-
4
- /**
5
- * Start heartbeat watcher timer.
6
- */
7
- export function startHeartbeatWatcher(scheduler, intervalMs = 30000) {
8
- stopHeartbeatWatcher(scheduler);
9
- scheduler.heartbeatTimer = setInterval(() => scheduler.checkHeartbeats(), intervalMs);
10
- if (scheduler.heartbeatTimer && typeof scheduler.heartbeatTimer.unref === 'function') {
11
- scheduler.heartbeatTimer.unref();
12
- }
13
- }
14
-
15
- /**
16
- * Stop heartbeat watcher timer.
17
- */
18
- export function stopHeartbeatWatcher(scheduler) {
19
- if (scheduler.heartbeatTimer) {
20
- clearInterval(scheduler.heartbeatTimer);
21
- scheduler.heartbeatTimer = null;
22
- }
23
- }
24
-
25
- /**
26
- * Check active tasks for missed heartbeats.
27
- */
28
- export async function checkHeartbeats(scheduler) {
29
- const now = Date.now();
30
- const tasks = typeof scheduler.store.list === 'function' ? scheduler.store.list({ status: 'active' }) : Array.from(scheduler.store.tasks.values());
31
- for (const task of tasks) {
32
- if (task.status !== 'active' || !task.heartbeatIntervalSeconds || task.heartbeatIntervalSeconds <= 0) {
33
- continue;
34
- }
35
- const grace = task.gracePeriodSeconds !== undefined ? task.gracePeriodSeconds : 300;
36
- const last = task.lastPingAt || task.createdAt || now;
37
- const deadline = last + (task.heartbeatIntervalSeconds + grace) * 1000;
38
-
39
- if (now > deadline && !task.heartbeatAlerted) {
40
- task.heartbeatAlerted = true;
41
- task.lastStatus = 'missed';
42
- task.updatedAt = now;
43
- scheduler.store.save();
44
-
45
- logger.warn(`[dsh-cron] Heartbeat missed for "${task.title}" (${task.id})!`);
46
- const runInfo = {
47
- at: now,
48
- status: 'missed',
49
- durationMs: 0,
50
- output: `Heartbeat missed: expected ping within ${task.heartbeatIntervalSeconds + grace}s, last ping was at ${new Date(last).toISOString()}`,
51
- error: 'Heartbeat deadline exceeded',
52
- usage: { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0 },
53
- costUsd: 0,
54
- };
55
-
56
- bestEffort('record-run', () => scheduler.store.recordRun(task.id, runInfo), scheduler.logger);
57
-
58
- await scheduler.deliverNotifications(task, runInfo);
59
-
60
- if (task.onFailure && String(task.onFailure).trim()) {
61
- const targetId = task.onFailure.trim();
62
- scheduler.runNow(targetId, {
63
- chainDepth: 1,
64
- prevOutput: runInfo.output,
65
- prevTaskId: task.id,
66
- }).catch((err) => logger.warn('[dsh-cron] heartbeat onFailure trigger error:', err.message));
67
- }
68
- }
69
- }
70
- }
71
-
72
- /**
73
- * Pause a task and abort any active execution.
74
- */
75
- export function pauseTask(scheduler, taskId, reason = null) {
76
- const task = scheduler.store.get(taskId);
77
- if (!task) return null;
78
- task.status = 'paused';
79
- if (task.oneShot) {
80
- task.targetTimestamp = task.targetTimestamp || task.nextRunAt;
81
- }
82
- task.nextRunAt = null;
83
- if (reason) {
84
- task.pausedReason = reason;
85
- }
86
- scheduler.clearRetryTimer(taskId);
87
- if (scheduler.jobs.has(taskId)) {
88
- scheduler.jobs.get(taskId).stop();
89
- scheduler.jobs.delete(taskId);
90
- }
91
- if (scheduler.timers.has(taskId)) {
92
- clearTimeout(scheduler.timers.get(taskId));
93
- scheduler.timers.delete(taskId);
94
- }
95
- if (scheduler.watcherManager) {
96
- scheduler.watcherManager.unwatchTask(taskId);
97
- }
98
- const running = scheduler.running.get(taskId);
99
- if (running) {
100
- bestEffort('abort-paused-task', () => running.controller.abort(new Error('Task paused')), scheduler.logger);
101
- scheduler.running.delete(taskId);
102
- }
103
- scheduler.queue = scheduler.queue.filter((item) => item.taskId !== taskId);
104
- scheduler.store.set(task);
105
- return task;
106
- }
107
-
108
- /**
109
- * Remove a scheduled task from timers, jobs, and run queue.
110
- */
111
- export function removeTask(scheduler, taskId) {
112
- scheduler.clearRetryTimer(taskId);
113
- if (scheduler.jobs.has(taskId)) {
114
- scheduler.jobs.get(taskId).stop();
115
- scheduler.jobs.delete(taskId);
116
- }
117
- if (scheduler.timers.has(taskId)) {
118
- clearTimeout(scheduler.timers.get(taskId));
119
- scheduler.timers.delete(taskId);
120
- }
121
- if (scheduler.watcherManager) {
122
- scheduler.watcherManager.unwatchTask(taskId);
123
- }
124
- const running = scheduler.running.get(taskId);
125
- if (running) {
126
- bestEffort('abort-deleted-task', () => running.controller.abort(new Error('Task deleted')), scheduler.logger);
127
- scheduler.running.delete(taskId);
128
- }
129
- scheduler.queue = scheduler.queue.filter((item) => item.taskId !== taskId);
130
- }
131
-
132
- /**
133
- * Resume a paused task and re-arm schedule.
134
- */
135
- export function resumeTask(scheduler, taskId) {
136
- const task = scheduler.store.get(taskId);
137
- if (!task) return null;
138
- task.status = 'active';
139
- delete task.pausedReason;
140
- scheduler.scheduleTask(task);
141
- scheduler.store.set(task);
142
- return task;
143
- }
144
-
145
- /**
146
- * Toggle task state between active and paused.
147
- */
148
- export function toggleTask(scheduler, taskId) {
149
- const task = scheduler.store.get(taskId);
150
- if (!task) return null;
151
- if (task.status === 'active') {
152
- return scheduler.pauseTask(taskId);
153
- } else {
154
- return scheduler.resumeTask(taskId);
155
- }
156
- }
157
-
1
+ export * from './scheduler/scheduler-lifecycle.js';