@goodandready/dsh-cron 0.3.25 → 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 (82) 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 -191
  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/commands.js +200 -0
  32. package/lib/durable-journal.js +1 -188
  33. package/lib/external-api.js +1 -74
  34. package/lib/index.js +17 -0
  35. package/lib/metrics.js +1 -125
  36. package/lib/routes/api-actions.js +317 -0
  37. package/lib/routes/api-collection.js +250 -0
  38. package/lib/routes/api-helpers.js +100 -0
  39. package/lib/routes/api-import-export.js +155 -0
  40. package/lib/routes/api-item.js +191 -0
  41. package/lib/routes/api-task-webhook.js +247 -0
  42. package/lib/routes/api-webhook.js +183 -0
  43. package/lib/routes/api.js +178 -0
  44. package/lib/routes/external-api.js +74 -0
  45. package/lib/routes/index.js +11 -0
  46. package/lib/routes/metrics.js +125 -0
  47. package/lib/routes/routes.js +549 -0
  48. package/lib/routes.js +1 -549
  49. package/lib/runner-external.js +1 -352
  50. package/lib/runner-worktree.js +1 -90
  51. package/lib/runner.js +1 -881
  52. package/lib/runtimes/index.js +4 -0
  53. package/lib/runtimes/runner-external.js +352 -0
  54. package/lib/runtimes/runner-worktree.js +90 -0
  55. package/lib/runtimes/runner.js +881 -0
  56. package/lib/runtimes/runtimes.js +198 -0
  57. package/lib/runtimes.js +1 -198
  58. package/lib/scheduler/index.js +6 -0
  59. package/lib/scheduler/scheduler-assists.js +142 -0
  60. package/lib/scheduler/scheduler-completion.js +417 -0
  61. package/lib/scheduler/scheduler-execution.js +360 -0
  62. package/lib/scheduler/scheduler-lifecycle.js +157 -0
  63. package/lib/scheduler/scheduler-preflight.js +195 -0
  64. package/lib/scheduler/scheduler.js +751 -0
  65. package/lib/scheduler-assists.js +1 -142
  66. package/lib/scheduler-completion.js +1 -417
  67. package/lib/scheduler-execution.js +1 -360
  68. package/lib/scheduler-lifecycle.js +1 -157
  69. package/lib/scheduler-preflight.js +1 -195
  70. package/lib/scheduler.js +1 -751
  71. package/lib/store/artifacts.js +200 -0
  72. package/lib/store/chain-checkpoints.js +223 -0
  73. package/lib/store/durable-journal.js +188 -0
  74. package/lib/store/index.js +6 -0
  75. package/lib/store/store.js +1179 -0
  76. package/lib/store/task-schema.js +180 -0
  77. package/lib/store/task-transfer.js +310 -0
  78. package/lib/store.js +2 -1179
  79. package/lib/task-schema.js +1 -180
  80. package/lib/task-transfer.js +1 -310
  81. package/lib/tui-settings.js +157 -0
  82. package/package.json +2 -1
@@ -0,0 +1,195 @@
1
+ import { exec } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import { executeSwarmTask } from '../swarm.js';
4
+ import { logger } from '../logger.js';
5
+ import { resolveTaskForExecution } from '../prompt-interpolation.js';
6
+
7
+ /** Task types that talk to a model and can therefore use a fallback model. */
8
+ export const AGENT_TASK_TYPES = ['llm', 'skill', 'workflow'];
9
+
10
+ /** Sum two usage records, so a fallback attempt is not lost in accounting. */
11
+ export function addUsage(a, b) {
12
+ return {
13
+ inputTokens: (a?.inputTokens || 0) + (b?.inputTokens || 0),
14
+ outputTokens: (a?.outputTokens || 0) + (b?.outputTokens || 0),
15
+ cacheReadTokens: (a?.cacheReadTokens || 0) + (b?.cacheReadTokens || 0),
16
+ };
17
+ }
18
+
19
+ /**
20
+ * Execute pre-flight check gate before running task body.
21
+ */
22
+ export async function executePreflight(task) {
23
+ const type = String(task.preflightType || 'none').trim().toLowerCase();
24
+ const target = String(task.preflightTarget || '').trim();
25
+ if (!type || type === 'none') {
26
+ return { ok: true };
27
+ }
28
+
29
+ const KNOWN_PREFLIGHT_TYPES = ['http', 'command', 'shell', 'disk'];
30
+ if (!KNOWN_PREFLIGHT_TYPES.includes(type)) {
31
+ return { ok: false, reason: `Unknown preflight type "${type}"` };
32
+ }
33
+
34
+ if (!target) {
35
+ return { ok: false, reason: `Preflight target is required for type "${type}"` };
36
+ }
37
+
38
+ if (type === 'http') {
39
+ let timeout;
40
+ try {
41
+ const controller = new AbortController();
42
+ timeout = setTimeout(() => controller.abort(), 5000);
43
+ const res = await fetch(target, { signal: controller.signal });
44
+ if (res.ok) return { ok: true };
45
+ return { ok: false, reason: `HTTP check returned status ${res.status}` };
46
+ } catch (err) {
47
+ return { ok: false, reason: `HTTP check failed: ${err.message}` };
48
+ } finally {
49
+ if (timeout) clearTimeout(timeout);
50
+ }
51
+ }
52
+
53
+ if (type === 'command' || type === 'shell') {
54
+ return new Promise((resolve) => {
55
+ exec(target, { timeout: 5000 }, (err, stdout, stderr) => {
56
+ if (err) {
57
+ resolve({ ok: false, reason: `Preflight command failed (code ${err.code || 1}): ${(stderr || stdout || err.message).trim().slice(0, 150)}` });
58
+ } else {
59
+ resolve({ ok: true });
60
+ }
61
+ });
62
+ });
63
+ }
64
+
65
+ if (type === 'disk') {
66
+ let diskPath = task.cwd || process.cwd();
67
+ let requirement = target;
68
+
69
+ // Check for "path:requirement", taking care not to split Windows drive letters (e.g. C:\:10%)
70
+ const lastColon = target.lastIndexOf(':');
71
+ if (lastColon > 0) {
72
+ const candidatePath = target.slice(0, lastColon).trim();
73
+ const candidateReq = target.slice(lastColon + 1).trim();
74
+ if (candidateReq) {
75
+ diskPath = candidatePath;
76
+ requirement = candidateReq;
77
+ }
78
+ }
79
+
80
+ const isPercent = requirement.endsWith('%');
81
+ const numericVal = parseFloat(requirement);
82
+ if (isNaN(numericVal) || numericVal <= 0) {
83
+ return { ok: false, reason: `Invalid preflight disk target requirement: "${requirement}"` };
84
+ }
85
+
86
+ try {
87
+ if (typeof fs.statfsSync !== 'function') {
88
+ return { ok: false, reason: 'statfsSync is not available on this platform' };
89
+ }
90
+ const stats = fs.statfsSync(diskPath);
91
+ if (isPercent) {
92
+ const freePercent = stats.blocks > 0 ? (stats.bavail / stats.blocks) * 100 : 0;
93
+ if (freePercent < numericVal) {
94
+ return { ok: false, reason: `Free disk space on "${diskPath}" (${freePercent.toFixed(1)}%) is below required ${numericVal}%` };
95
+ }
96
+ } else {
97
+ const freeMb = Math.round((stats.bavail * stats.bsize) / (1024 * 1024));
98
+ if (freeMb < numericVal) {
99
+ return { ok: false, reason: `Free disk space on "${diskPath}" (${freeMb} MB) is below required ${numericVal} MB` };
100
+ }
101
+ }
102
+ return { ok: true };
103
+ } catch (err) {
104
+ return { ok: false, reason: `Preflight disk check failed: filesystem "${diskPath}" not accessible (${err.message})` };
105
+ }
106
+ }
107
+
108
+ return { ok: false, reason: `Unsupported preflight type "${type}"` };
109
+ }
110
+
111
+ /** Only agent-mediated types use a model, and only a failed run falls back. */
112
+ export function shouldUseFallback(task, outcome) {
113
+ if (!task || !task.fallbackModel) return false;
114
+ if (!AGENT_TASK_TYPES.includes(task.type || 'llm')) return false;
115
+ return outcome.status === 'error' || outcome.status === 'timeout';
116
+ }
117
+
118
+ /** Run the task body once and normalise whatever the executor returned. */
119
+ export async function executeOnce(scheduler, task, signal, options = {}) {
120
+ const result = {
121
+ status: 'success',
122
+ output: '',
123
+ error: null,
124
+ usage: { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0 },
125
+ costUsd: 0,
126
+ sessionId: null,
127
+ model: task.model || '',
128
+ fallback: false,
129
+ };
130
+ if (task.type === 'swarm') {
131
+ try {
132
+ const swarmRes = await executeSwarmTask(task, {
133
+ executeFn: (subtask) => scheduler.executeFn(subtask, { signal, ...options }),
134
+ askModel: scheduler.askModel,
135
+ signal,
136
+ logger,
137
+ });
138
+ result.output = swarmRes.output || '';
139
+ result.status = swarmRes.status;
140
+ result.subtaskRuns = swarmRes.subtaskRuns || [];
141
+ result.swarmStats = swarmRes.stats;
142
+ return result;
143
+ } catch (err) {
144
+ result.error = err.message || String(err);
145
+ result.status = 'error';
146
+ return result;
147
+ }
148
+ }
149
+ if (typeof scheduler.executeFn !== 'function') return result;
150
+ const executableTask = resolveTaskForExecution(task, options);
151
+ try {
152
+ const res = await scheduler.executeFn(executableTask, { signal, ...options });
153
+ if (typeof res === 'object' && res !== null) {
154
+ result.output = res.output || '';
155
+ result.usage = res.usage || result.usage;
156
+ result.costUsd = res.costUsd || 0;
157
+ result.sessionId = res.sessionId || null;
158
+ if (res.model) result.model = res.model;
159
+ } else {
160
+ result.output = String(res || '');
161
+ }
162
+ } catch (err) {
163
+ result.error = err.message || String(err);
164
+ const timedOut = result.error.toLowerCase().includes('timed out') || result.error.toLowerCase().includes('timeout');
165
+ result.status = timedOut ? 'timeout' : 'error';
166
+ if (err.usage) result.usage = err.usage;
167
+ if (typeof err.costUsd === 'number') result.costUsd = err.costUsd;
168
+ if (err.sessionId) result.sessionId = err.sessionId;
169
+ }
170
+ return result;
171
+ }
172
+
173
+ /**
174
+ * Run the task, retrying once on the configured fallback model when the primary attempt failed (#45).
175
+ */
176
+ export async function executeWithFallback(scheduler, task, signal, options = {}) {
177
+ const primary = await scheduler.executeOnce(task, signal, options);
178
+ if (!scheduler.shouldUseFallback(task, primary)) return primary;
179
+
180
+ const fallbackTask = {
181
+ ...task,
182
+ provider: task.fallbackProvider || task.provider,
183
+ model: task.fallbackModel,
184
+ };
185
+ logger.info(`[dsh-cron] task "${task.title}" failed on ${task.model || 'the default model'}, retrying once on ${task.fallbackModel}`);
186
+ const secondary = await scheduler.executeOnce(fallbackTask, signal, options);
187
+ return {
188
+ ...secondary,
189
+ fallback: true,
190
+ usage: addUsage(primary.usage, secondary.usage),
191
+ costUsd: Number((primary.costUsd + secondary.costUsd).toFixed(6)),
192
+ primaryModel: task.model || '',
193
+ };
194
+ }
195
+