@goodandready/dsh-cron 0.2.42 → 0.2.44
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 +15 -7
- package/lib/index.js +8 -3
- package/lib/runner.js +7 -2
- package/lib/settings.js +11 -18
- package/lib/store.js +28 -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,9 +1507,18 @@ window.__ModuleLoader__.load({
|
|
|
1507
1507
|
...options,
|
|
1508
1508
|
signal: controller ? controller.signal : options.signal,
|
|
1509
1509
|
});
|
|
1510
|
-
|
|
1510
|
+
let data = {};
|
|
1511
|
+
try {
|
|
1512
|
+
data = await res.json();
|
|
1513
|
+
} catch (jsonErr) {
|
|
1514
|
+
// Re-throw if body reading aborted/timed out (#242)
|
|
1515
|
+
if (jsonErr && (jsonErr.name === 'AbortError' || controller?.signal.aborted)) {
|
|
1516
|
+
throw jsonErr;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
return { res, data };
|
|
1511
1520
|
} catch (err) {
|
|
1512
|
-
if (err && err.name === 'AbortError') {
|
|
1521
|
+
if (err && (err.name === 'AbortError' || controller?.signal.aborted)) {
|
|
1513
1522
|
const timeoutErr = new Error(`Request timed out after ${Math.round(timeoutMs / 1000)}s`);
|
|
1514
1523
|
timeoutErr.isTimeout = true;
|
|
1515
1524
|
throw timeoutErr;
|
|
@@ -1519,13 +1528,13 @@ window.__ModuleLoader__.load({
|
|
|
1519
1528
|
if (timer) clearTimeout(timer);
|
|
1520
1529
|
}
|
|
1521
1530
|
};
|
|
1531
|
+
const fetchWithTimeout = fetchJsonWithTimeout;
|
|
1522
1532
|
|
|
1523
1533
|
const handleToggleTask = async (id) => {
|
|
1524
1534
|
if (actionInProgressRef.current.has(id)) return;
|
|
1525
1535
|
actionInProgressRef.current.add(id);
|
|
1526
1536
|
try {
|
|
1527
|
-
const res = await
|
|
1528
|
-
const data = await res.json().catch(() => ({}));
|
|
1537
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(id) + '/toggle', { method: 'POST' }, 15000);
|
|
1529
1538
|
if (!res.ok || (data && data.ok === false)) {
|
|
1530
1539
|
const msg = (data && data.error) || `Toggle failed (HTTP ${res.status})`;
|
|
1531
1540
|
alert(msg);
|
|
@@ -1547,8 +1556,7 @@ window.__ModuleLoader__.load({
|
|
|
1547
1556
|
const prevStatus = prevTask ? prevTask.lastStatus : null;
|
|
1548
1557
|
setTasks(prev => prev.map(t2 => t2.id === id ? { ...t2, lastStatus: 'running' } : t2));
|
|
1549
1558
|
try {
|
|
1550
|
-
const res = await
|
|
1551
|
-
const data = await res.json().catch(() => ({}));
|
|
1559
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(id) + '/run', { method: 'POST' }, 15000);
|
|
1552
1560
|
if (!res.ok || (data && data.ok === false)) {
|
|
1553
1561
|
setTasks(prev => prev.map(t2 => t2.id === id ? { ...t2, lastStatus: prevStatus } : t2));
|
|
1554
1562
|
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';
|
|
@@ -79,6 +80,7 @@ import {
|
|
|
79
80
|
sanitizeSettingsPayload,
|
|
80
81
|
unwrapConfig,
|
|
81
82
|
unwrapConfigValue,
|
|
83
|
+
plainConfig,
|
|
82
84
|
} from './settings.js';
|
|
83
85
|
import { registerRoutes } from './routes.js';
|
|
84
86
|
|
|
@@ -89,6 +91,7 @@ export {
|
|
|
89
91
|
sanitizeSettingsPayload,
|
|
90
92
|
unwrapConfig,
|
|
91
93
|
unwrapConfigValue,
|
|
94
|
+
plainConfig,
|
|
92
95
|
};
|
|
93
96
|
|
|
94
97
|
export const name = '@goodandready/dsh-cron';
|
|
@@ -118,8 +121,9 @@ const pkgVersion = JSON.parse(fs.readFileSync(pkgUrl, 'utf8')).version || '0.2.2
|
|
|
118
121
|
|
|
119
122
|
export function apply(ctx, rawConfig) {
|
|
120
123
|
if (ctx && ctx.logger) setLogger(ctx.logger);
|
|
121
|
-
const config =
|
|
122
|
-
const
|
|
124
|
+
const config = plainConfig(rawConfig);
|
|
125
|
+
const storePath = (config && (config.cronDir || config.storePath)) ? (config.storePath || path.join(config.cronDir, 'tasks.json')) : undefined;
|
|
126
|
+
const store = new TaskStore(storePath);
|
|
123
127
|
|
|
124
128
|
// Sync settings declared in the profile config into the store (#184)
|
|
125
129
|
if (config && typeof config === 'object') {
|
|
@@ -210,7 +214,8 @@ export function apply(ctx, rawConfig) {
|
|
|
210
214
|
let activeVolatileConfig = {};
|
|
211
215
|
function syncVolatileConfig(updatedConfig) {
|
|
212
216
|
try {
|
|
213
|
-
const
|
|
217
|
+
const isPlainConfigObject = updatedConfig && typeof updatedConfig === 'object' && !Array.isArray(updatedConfig);
|
|
218
|
+
const cfg = isPlainConfigObject ? { ...(ctx.config || config || {}), ...updatedConfig } : (ctx.config || config || {});
|
|
214
219
|
const unwrapped = unwrapConfig(cfg);
|
|
215
220
|
activeVolatileConfig = { ...activeVolatileConfig, ...unwrapped };
|
|
216
221
|
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/settings.js
CHANGED
|
@@ -53,28 +53,21 @@ export const Config = z.object({
|
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
*
|
|
56
|
+
* Deeply unwrap Cosmokit/Schemastery Volatile boxes into plain primitives/objects/arrays (#282).
|
|
57
57
|
*/
|
|
58
|
-
export function
|
|
59
|
-
if (
|
|
60
|
-
|
|
58
|
+
export function plainConfig(value) {
|
|
59
|
+
if (value === null || value === undefined) return value;
|
|
60
|
+
if (typeof value === 'function') return plainConfig(value());
|
|
61
|
+
if (typeof value.get === 'function') return plainConfig(value.get());
|
|
62
|
+
if (Array.isArray(value)) return value.map(plainConfig);
|
|
63
|
+
if (typeof value === 'object') {
|
|
64
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, plainConfig(v)]));
|
|
61
65
|
}
|
|
62
|
-
return
|
|
66
|
+
return value;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
export
|
|
66
|
-
|
|
67
|
-
if (typeof cfg.get === 'function') return unwrapConfig(cfg.get());
|
|
68
|
-
return new Proxy(cfg, {
|
|
69
|
-
get(target, prop, receiver) {
|
|
70
|
-
const val = Reflect.get(target, prop, receiver);
|
|
71
|
-
if (val && typeof val === 'object' && typeof val.get === 'function') {
|
|
72
|
-
return val.get();
|
|
73
|
-
}
|
|
74
|
-
return val;
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
}
|
|
69
|
+
export const unwrapConfigValue = plainConfig;
|
|
70
|
+
export const unwrapConfig = plainConfig;
|
|
78
71
|
|
|
79
72
|
export const SETTINGS_SYNC_KEYS = [
|
|
80
73
|
'botToken', 'botTokenRef', 'telegramWebhookSecret', 'chatId', 'notifyTelegram', 'onlyOnFailure', 'kanbanBaseUrl',
|
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,34 @@ 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
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
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 only in that case (#244)
|
|
213
|
+
if (err.code === 'ENOENT' && (this._lastSyncVersion || 0) >= version) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
throw err;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
this._diskVersion = version;
|
|
220
|
-
this._lastWrittenVersion = version;
|
|
221
|
-
|
|
222
219
|
const now = Date.now();
|
|
223
220
|
if (now - this._lastBackupTime > 60000) {
|
|
224
221
|
this._lastBackupTime = now;
|
|
@@ -242,6 +239,14 @@ export class TaskStore {
|
|
|
242
239
|
this._saveTimer = null;
|
|
243
240
|
}
|
|
244
241
|
const version = ++this._saveVersion;
|
|
242
|
+
this._lastSyncVersion = version;
|
|
243
|
+
|
|
244
|
+
// Immediately cancel and unlink any in-flight async tmp file so async rename cannot touch disk (#244)
|
|
245
|
+
if (this._currentAsyncTmp) {
|
|
246
|
+
const inFlightTmp = this._currentAsyncTmp;
|
|
247
|
+
this._currentAsyncTmp = null;
|
|
248
|
+
bestEffort('unlink-async-tmp', () => fs.unlinkSync(inFlightTmp));
|
|
249
|
+
}
|
|
245
250
|
try {
|
|
246
251
|
const dir = path.dirname(this.filePath);
|
|
247
252
|
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.44",
|
|
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",
|