@goodandready/dsh-cron 0.2.22 → 0.2.23
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/burn-guard.js +2 -4
- package/lib/client.js +14 -2
- package/lib/scheduler-execution.js +14 -3
- package/lib/store.js +42 -6
- package/package.json +1 -1
package/lib/burn-guard.js
CHANGED
|
@@ -132,7 +132,7 @@ export async function checkAndApplyBurnGuard(scheduler, taskId, outcome) {
|
|
|
132
132
|
scheduler.pauseTask(taskId, guard.reason);
|
|
133
133
|
|
|
134
134
|
const alertMessage = formatBurnGuardAlert(task, guard);
|
|
135
|
-
|
|
135
|
+
await bestEffort('burn-guard-notify', async () => {
|
|
136
136
|
if (typeof scheduler.deliverNotifications === 'function') {
|
|
137
137
|
const alertRunInfo = {
|
|
138
138
|
output: alertMessage,
|
|
@@ -144,9 +144,7 @@ export async function checkAndApplyBurnGuard(scheduler, taskId, outcome) {
|
|
|
144
144
|
};
|
|
145
145
|
await scheduler.deliverNotifications({ ...task, status: 'paused', pausedReason: guard.reason }, alertRunInfo);
|
|
146
146
|
}
|
|
147
|
-
}
|
|
148
|
-
bestEffort('burn-guard-notify', () => {}, scheduler.logger);
|
|
149
|
-
}
|
|
147
|
+
}, scheduler.logger);
|
|
150
148
|
|
|
151
149
|
return guard;
|
|
152
150
|
}
|
package/lib/client.js
CHANGED
|
@@ -3380,7 +3380,7 @@ window.__ModuleLoader__.load({
|
|
|
3380
3380
|
const [updateState, setUpdateState] = React.useState({
|
|
3381
3381
|
checking: false,
|
|
3382
3382
|
updating: false,
|
|
3383
|
-
currentVersion: '0.2.
|
|
3383
|
+
currentVersion: '0.2.23',
|
|
3384
3384
|
latestVersion: undefined,
|
|
3385
3385
|
updateAvailable: false,
|
|
3386
3386
|
notice: null,
|
|
@@ -3901,7 +3901,19 @@ window.__ModuleLoader__.load({
|
|
|
3901
3901
|
const state = { toggle, openTaskSignal, root: undefined, listEl: undefined, headEl: undefined, open: false };
|
|
3902
3902
|
try { state.open = window.localStorage.getItem(SIDEBAR_JOBS_KEY) === '1'; } catch (err) { /* localStorage read failed */ }
|
|
3903
3903
|
|
|
3904
|
-
|
|
3904
|
+
let rafPending = false;
|
|
3905
|
+
const schedulePlace = () => {
|
|
3906
|
+
if (state.root !== undefined && state.root.isConnected) return;
|
|
3907
|
+
if (rafPending) return;
|
|
3908
|
+
rafPending = true;
|
|
3909
|
+
const scheduleFn = typeof requestAnimationFrame === 'function' ? requestAnimationFrame : (cb) => setTimeout(cb, 16);
|
|
3910
|
+
scheduleFn(() => {
|
|
3911
|
+
rafPending = false;
|
|
3912
|
+
placeSidebarJobs(state, label);
|
|
3913
|
+
});
|
|
3914
|
+
};
|
|
3915
|
+
|
|
3916
|
+
const observer = new MutationObserver(schedulePlace);
|
|
3905
3917
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
3906
3918
|
placeSidebarJobs(state, label);
|
|
3907
3919
|
loadSidebarJobs(state);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { checkAndApplyBurnGuard } from './burn-guard.js';
|
|
1
|
+
import { checkAndApplyBurnGuard, checkTaskBudgetLimits } from './burn-guard.js';
|
|
2
2
|
import { resolveTaskForExecution } from './prompt-interpolation.js';
|
|
3
3
|
import { bestEffort } from './best-effort.js';
|
|
4
4
|
import { exec } from 'node:child_process';
|
|
@@ -66,8 +66,8 @@ export async function executePreflight(task) {
|
|
|
66
66
|
}
|
|
67
67
|
return { ok: true };
|
|
68
68
|
} catch (err) {
|
|
69
|
-
|
|
70
|
-
return { ok: true };
|
|
69
|
+
// Fail-open if statfsSync is unavailable or throws
|
|
70
|
+
return { ok: true };
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -185,6 +185,17 @@ export async function executeTask(scheduler, taskId, options = {}) {
|
|
|
185
185
|
const task = scheduler.store.get(taskId);
|
|
186
186
|
if (!task) return null;
|
|
187
187
|
|
|
188
|
+
// Pre-flight Burn Guard check (#181): prevent executing task if budget already breached
|
|
189
|
+
const runs = (scheduler.store.history && scheduler.store.history.get(taskId)) || [];
|
|
190
|
+
const guard = checkTaskBudgetLimits(task, runs);
|
|
191
|
+
if (guard.exceeded) {
|
|
192
|
+
console.warn(`[dsh-cron] Burn Guard pre-flight blocked task "${task.title}" (${taskId}): ${guard.reason}`);
|
|
193
|
+
scheduler.pauseTask(taskId, guard.reason);
|
|
194
|
+
scheduler.recordSkipped(taskId, `Burn Guard pre-flight pause: ${guard.reason}`);
|
|
195
|
+
checkAndApplyBurnGuard(scheduler, taskId, { status: 'error', costUsd: 0, durationMs: 0 }).catch(() => {});
|
|
196
|
+
return { ok: false, status: 'error', error: guard.reason, preflightBlocked: true };
|
|
197
|
+
}
|
|
198
|
+
|
|
188
199
|
if (!options.isRetry && task.attempts) {
|
|
189
200
|
task.attempts = 0;
|
|
190
201
|
scheduler.store.set(task);
|
package/lib/store.js
CHANGED
|
@@ -34,6 +34,8 @@ export class TaskStore {
|
|
|
34
34
|
this.tasks = new Map();
|
|
35
35
|
this.history = new Map();
|
|
36
36
|
this.settings = {};
|
|
37
|
+
this._lastBackupTime = 0;
|
|
38
|
+
this._saveTimer = null;
|
|
37
39
|
this._savePromise = Promise.resolve();
|
|
38
40
|
this.init();
|
|
39
41
|
}
|
|
@@ -102,7 +104,36 @@ export class TaskStore {
|
|
|
102
104
|
}
|
|
103
105
|
}
|
|
104
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Schedule a debounced save to avoid synchronous event loop blocking on high-frequency run completions.
|
|
109
|
+
*/
|
|
110
|
+
scheduleSave(delay = 50) {
|
|
111
|
+
if (this._saveTimer) return;
|
|
112
|
+
this._saveTimer = setTimeout(() => {
|
|
113
|
+
this._saveTimer = null;
|
|
114
|
+
this.save();
|
|
115
|
+
}, delay);
|
|
116
|
+
if (typeof this._saveTimer.unref === 'function') {
|
|
117
|
+
this._saveTimer.unref();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Synchronously flush any pending debounced save to disk.
|
|
123
|
+
*/
|
|
124
|
+
flushSync() {
|
|
125
|
+
if (this._saveTimer) {
|
|
126
|
+
clearTimeout(this._saveTimer);
|
|
127
|
+
this._saveTimer = null;
|
|
128
|
+
}
|
|
129
|
+
this.save();
|
|
130
|
+
}
|
|
131
|
+
|
|
105
132
|
save() {
|
|
133
|
+
if (this._saveTimer) {
|
|
134
|
+
clearTimeout(this._saveTimer);
|
|
135
|
+
this._saveTimer = null;
|
|
136
|
+
}
|
|
106
137
|
try {
|
|
107
138
|
const dir = path.dirname(this.filePath);
|
|
108
139
|
if (!fs.existsSync(dir)) {
|
|
@@ -120,7 +151,12 @@ export class TaskStore {
|
|
|
120
151
|
const tmp = `${this.filePath}.tmp.${process.pid}_${Date.now()}_${rand}`;
|
|
121
152
|
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
|
|
122
153
|
fs.renameSync(tmp, this.filePath);
|
|
123
|
-
|
|
154
|
+
// Throttled backup (.bak): avoid expensive copyFileSync on every micro-save
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
if (now - this._lastBackupTime > 60000) {
|
|
157
|
+
this._lastBackupTime = now;
|
|
158
|
+
bestEffort('backup-on-save', () => fs.copyFileSync(this.filePath, `${this.filePath}.bak`));
|
|
159
|
+
}
|
|
124
160
|
} catch (err) {
|
|
125
161
|
console.error('[dsh-cron] store save error:', err.message);
|
|
126
162
|
}
|
|
@@ -290,8 +326,8 @@ export class TaskStore {
|
|
|
290
326
|
agentPreset: task.agentPreset !== undefined ? String(task.agentPreset).trim() : (prev.agentPreset || ''),
|
|
291
327
|
targetSessionId: task.targetSessionId !== undefined ? String(task.targetSessionId).trim() : (prev.targetSessionId || ''),
|
|
292
328
|
targetSessionReset: task.targetSessionReset !== undefined ? String(task.targetSessionReset).trim() : (prev.targetSessionReset || 'never'),
|
|
293
|
-
totalTokens: prev.totalTokens || 0,
|
|
294
|
-
totalCostUsd: prev.totalCostUsd || 0,
|
|
329
|
+
totalTokens: task.totalTokens !== undefined ? Number(task.totalTokens) : (prev.totalTokens || 0),
|
|
330
|
+
totalCostUsd: task.totalCostUsd !== undefined ? Number(task.totalCostUsd) : (prev.totalCostUsd || 0),
|
|
295
331
|
createdAt: prev.createdAt || task.createdAt || now,
|
|
296
332
|
updatedAt: now,
|
|
297
333
|
lastRunAt: task.lastRunAt !== undefined ? task.lastRunAt : (prev.lastRunAt || null),
|
|
@@ -379,7 +415,7 @@ export class TaskStore {
|
|
|
379
415
|
this._archiveHistoryRuns(id, overflow);
|
|
380
416
|
}
|
|
381
417
|
this.history.set(id, runs);
|
|
382
|
-
this.
|
|
418
|
+
this.scheduleSave();
|
|
383
419
|
}
|
|
384
420
|
|
|
385
421
|
/**
|
|
@@ -399,10 +435,10 @@ export class TaskStore {
|
|
|
399
435
|
if (archive[id].length > 1000) {
|
|
400
436
|
archive[id] = archive[id].slice(-1000);
|
|
401
437
|
}
|
|
402
|
-
// Write atomically with tmp file
|
|
438
|
+
// Write atomically with tmp file using compact JSON (avoiding mega-lines and null, 2 serialization bloat)
|
|
403
439
|
const rand = Math.random().toString(36).slice(2, 8);
|
|
404
440
|
const tmp = `${archivePath}.tmp.${process.pid}_${Date.now()}_${rand}`;
|
|
405
|
-
fs.writeFileSync(tmp, JSON.stringify(archive
|
|
441
|
+
fs.writeFileSync(tmp, JSON.stringify(archive), 'utf-8');
|
|
406
442
|
fs.renameSync(tmp, archivePath);
|
|
407
443
|
} catch (archiveErr) {
|
|
408
444
|
console.error('[dsh-cron] history archive error:', archiveErr.message);
|