@goodandready/dsh-cron 0.2.42 → 0.2.43
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/client.js +6 -6
- package/lib/index.js +5 -2
- package/lib/runner.js +7 -2
- package/lib/store.js +25 -23
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -1499,7 +1499,7 @@ window.__ModuleLoader__.load({
|
|
|
1499
1499
|
|
|
1500
1500
|
const actionInProgressRef = React.useRef(new Set());
|
|
1501
1501
|
|
|
1502
|
-
const
|
|
1502
|
+
const fetchJsonWithTimeout = async (url, options = {}, timeoutMs = 15000) => {
|
|
1503
1503
|
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
1504
1504
|
const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
1505
1505
|
try {
|
|
@@ -1507,7 +1507,8 @@ window.__ModuleLoader__.load({
|
|
|
1507
1507
|
...options,
|
|
1508
1508
|
signal: controller ? controller.signal : options.signal,
|
|
1509
1509
|
});
|
|
1510
|
-
|
|
1510
|
+
const data = await res.json().catch(() => ({}));
|
|
1511
|
+
return { res, data };
|
|
1511
1512
|
} catch (err) {
|
|
1512
1513
|
if (err && err.name === 'AbortError') {
|
|
1513
1514
|
const timeoutErr = new Error(`Request timed out after ${Math.round(timeoutMs / 1000)}s`);
|
|
@@ -1519,13 +1520,13 @@ window.__ModuleLoader__.load({
|
|
|
1519
1520
|
if (timer) clearTimeout(timer);
|
|
1520
1521
|
}
|
|
1521
1522
|
};
|
|
1523
|
+
const fetchWithTimeout = fetchJsonWithTimeout;
|
|
1522
1524
|
|
|
1523
1525
|
const handleToggleTask = async (id) => {
|
|
1524
1526
|
if (actionInProgressRef.current.has(id)) return;
|
|
1525
1527
|
actionInProgressRef.current.add(id);
|
|
1526
1528
|
try {
|
|
1527
|
-
const res = await
|
|
1528
|
-
const data = await res.json().catch(() => ({}));
|
|
1529
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(id) + '/toggle', { method: 'POST' }, 15000);
|
|
1529
1530
|
if (!res.ok || (data && data.ok === false)) {
|
|
1530
1531
|
const msg = (data && data.error) || `Toggle failed (HTTP ${res.status})`;
|
|
1531
1532
|
alert(msg);
|
|
@@ -1547,8 +1548,7 @@ window.__ModuleLoader__.load({
|
|
|
1547
1548
|
const prevStatus = prevTask ? prevTask.lastStatus : null;
|
|
1548
1549
|
setTasks(prev => prev.map(t2 => t2.id === id ? { ...t2, lastStatus: 'running' } : t2));
|
|
1549
1550
|
try {
|
|
1550
|
-
const res = await
|
|
1551
|
-
const data = await res.json().catch(() => ({}));
|
|
1551
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(id) + '/run', { method: 'POST' }, 15000);
|
|
1552
1552
|
if (!res.ok || (data && data.ok === false)) {
|
|
1553
1553
|
setTasks(prev => prev.map(t2 => t2.id === id ? { ...t2, lastStatus: prevStatus } : t2));
|
|
1554
1554
|
const msg = (data && data.error) || `Run failed (HTTP ${res.status})`;
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { bestEffort } from './best-effort.js';
|
|
3
4
|
import { setLogger, logger } from './logger.js';
|
|
4
5
|
import { registerPluginUpdater } from './updater.js';
|
|
@@ -119,7 +120,8 @@ const pkgVersion = JSON.parse(fs.readFileSync(pkgUrl, 'utf8')).version || '0.2.2
|
|
|
119
120
|
export function apply(ctx, rawConfig) {
|
|
120
121
|
if (ctx && ctx.logger) setLogger(ctx.logger);
|
|
121
122
|
const config = unwrapConfig(rawConfig);
|
|
122
|
-
const
|
|
123
|
+
const storePath = (config && (config.cronDir || config.storePath)) ? (config.storePath || path.join(config.cronDir, 'tasks.json')) : undefined;
|
|
124
|
+
const store = new TaskStore(storePath);
|
|
123
125
|
|
|
124
126
|
// Sync settings declared in the profile config into the store (#184)
|
|
125
127
|
if (config && typeof config === 'object') {
|
|
@@ -210,7 +212,8 @@ export function apply(ctx, rawConfig) {
|
|
|
210
212
|
let activeVolatileConfig = {};
|
|
211
213
|
function syncVolatileConfig(updatedConfig) {
|
|
212
214
|
try {
|
|
213
|
-
const
|
|
215
|
+
const isPlainConfigObject = updatedConfig && typeof updatedConfig === 'object' && !Array.isArray(updatedConfig);
|
|
216
|
+
const cfg = isPlainConfigObject ? { ...(ctx.config || config || {}), ...updatedConfig } : (ctx.config || config || {});
|
|
214
217
|
const unwrapped = unwrapConfig(cfg);
|
|
215
218
|
activeVolatileConfig = { ...activeVolatileConfig, ...unwrapped };
|
|
216
219
|
if (typeof unwrapped.maxConcurrent === 'number') {
|
package/lib/runner.js
CHANGED
|
@@ -449,8 +449,13 @@ export class SessionRunner {
|
|
|
449
449
|
}
|
|
450
450
|
}
|
|
451
451
|
|
|
452
|
-
|
|
453
|
-
|
|
452
|
+
const hasEventLog = typeof session?.snapshotEvents === 'function' || Array.isArray(session?.log) || Array.isArray(handle.agent?.log);
|
|
453
|
+
if (hasEventLog && !hasTerminalEvent) {
|
|
454
|
+
const err = new Error(
|
|
455
|
+
turnEvents.length === 0
|
|
456
|
+
? 'Agent turn produced no events before quiescence'
|
|
457
|
+
: 'Agent turn ended without terminal completion event'
|
|
458
|
+
);
|
|
454
459
|
err.turnFailed = true;
|
|
455
460
|
err.usage = usage;
|
|
456
461
|
err.costUsd = costUsd;
|
package/lib/store.js
CHANGED
|
@@ -179,7 +179,7 @@ export class TaskStore {
|
|
|
179
179
|
};
|
|
180
180
|
|
|
181
181
|
const writeOp = async () => {
|
|
182
|
-
if ((this._diskVersion || 0) >= version) {
|
|
182
|
+
if ((this._diskVersion || 0) >= version || (this._lastSyncVersion || 0) >= version) {
|
|
183
183
|
return;
|
|
184
184
|
}
|
|
185
185
|
const dir = path.dirname(this.filePath);
|
|
@@ -188,37 +188,31 @@ export class TaskStore {
|
|
|
188
188
|
|
|
189
189
|
const rand = Math.random().toString(36).slice(2, 8);
|
|
190
190
|
const tmp = `${this.filePath}.tmp.${process.pid}_${Date.now()}_${rand}`;
|
|
191
|
-
|
|
191
|
+
this._currentAsyncTmp = tmp;
|
|
192
|
+
|
|
193
|
+
// Use compact serialization without whitespace indentation to eliminate event loop lag on large stores (#244)
|
|
194
|
+
const payload = JSON.stringify(data);
|
|
192
195
|
await fs.promises.writeFile(tmp, payload, { encoding: 'utf-8', mode: 0o600 });
|
|
193
196
|
bestEffort('chmod-tmp', () => fs.chmodSync(tmp, 0o600));
|
|
194
197
|
|
|
195
|
-
if ((this._diskVersion || 0) >= version || this._saveVersion > version) {
|
|
198
|
+
if ((this._diskVersion || 0) >= version || (this._lastSyncVersion || 0) >= version || this._saveVersion > version) {
|
|
196
199
|
await fs.promises.unlink(tmp).catch(() => {});
|
|
200
|
+
if (this._currentAsyncTmp === tmp) this._currentAsyncTmp = null;
|
|
197
201
|
return;
|
|
198
202
|
}
|
|
199
203
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
settings: this.settings || {},
|
|
211
|
-
};
|
|
212
|
-
const randRep = Math.random().toString(36).slice(2, 8);
|
|
213
|
-
const tmpRep = `${this.filePath}.tmp.${process.pid}_${Date.now()}_${randRep}`;
|
|
214
|
-
fs.writeFileSync(tmpRep, JSON.stringify(syncData, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
215
|
-
fs.renameSync(tmpRep, this.filePath);
|
|
216
|
-
return;
|
|
204
|
+
try {
|
|
205
|
+
await fs.promises.rename(tmp, this.filePath);
|
|
206
|
+
bestEffort('chmod-store', () => fs.chmodSync(this.filePath, 0o600));
|
|
207
|
+
this._currentAsyncTmp = null;
|
|
208
|
+
this._diskVersion = version;
|
|
209
|
+
this._lastWrittenVersion = version;
|
|
210
|
+
} catch (err) {
|
|
211
|
+
this._currentAsyncTmp = null;
|
|
212
|
+
// If tmp was unlinked by newer sync save, rename will fail with ENOENT - safely ignore
|
|
213
|
+
if (err.code !== 'ENOENT') throw err;
|
|
217
214
|
}
|
|
218
215
|
|
|
219
|
-
this._diskVersion = version;
|
|
220
|
-
this._lastWrittenVersion = version;
|
|
221
|
-
|
|
222
216
|
const now = Date.now();
|
|
223
217
|
if (now - this._lastBackupTime > 60000) {
|
|
224
218
|
this._lastBackupTime = now;
|
|
@@ -242,6 +236,14 @@ export class TaskStore {
|
|
|
242
236
|
this._saveTimer = null;
|
|
243
237
|
}
|
|
244
238
|
const version = ++this._saveVersion;
|
|
239
|
+
this._lastSyncVersion = version;
|
|
240
|
+
|
|
241
|
+
// Immediately cancel and unlink any in-flight async tmp file so async rename cannot touch disk (#244)
|
|
242
|
+
if (this._currentAsyncTmp) {
|
|
243
|
+
const inFlightTmp = this._currentAsyncTmp;
|
|
244
|
+
this._currentAsyncTmp = null;
|
|
245
|
+
bestEffort('unlink-async-tmp', () => fs.unlinkSync(inFlightTmp));
|
|
246
|
+
}
|
|
245
247
|
try {
|
|
246
248
|
const dir = path.dirname(this.filePath);
|
|
247
249
|
if (!fs.existsSync(dir)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-cron",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.43",
|
|
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",
|