@goodandready/dsh-cron 0.1.23 → 0.1.24
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/docs/design/DESIGN.md +1 -0
- package/lib/client.js +22 -4
- package/lib/runner.js +22 -5
- package/lib/scheduler.js +15 -0
- package/lib/store.js +4 -1
- package/package.json +1 -1
package/docs/design/DESIGN.md
CHANGED
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
4. Выполнение по крону: срабатывает таймер croner -> запуск сессии агента или команды -> запись в историю -> доставка результата.
|
|
57
57
|
|
|
58
58
|
## Locked Design Decisions
|
|
59
|
+
- 2026-09-08 — Пакет надежности ядра (v0.1.24): буфер shell-задач 10МБ, bash/env, атомарное сохранение с PID, автоочистка сессий агента ephemeral/internal, аудит пропущенных запусков при рестарте (missed), фоновый поллинг UI (8с).
|
|
59
60
|
- 2026-09-03 — Публичный скоуп @goodandready/dsh-cron.
|
|
60
61
|
- 2026-09-03 — Оверлей открывается через mountSidebarEntry и mountScreen аналогично dsh-kanban.
|
|
61
62
|
- 2026-09-03 — Двойная кнопка «Создать ⌄» («Создать с DSH» и «Настроить вручную»).
|
package/lib/client.js
CHANGED
|
@@ -342,7 +342,23 @@ const React = window.React || require('react');
|
|
|
342
342
|
}
|
|
343
343
|
};
|
|
344
344
|
|
|
345
|
-
React.useEffect(() => {
|
|
345
|
+
React.useEffect(() => {
|
|
346
|
+
fetchTasks();
|
|
347
|
+
const pollInterval = setInterval(() => {
|
|
348
|
+
// Background polling without resetting loading spinner
|
|
349
|
+
fetch('/dsh-cron/tasks?status=' + encodeURIComponent(tab) + '&query=' + encodeURIComponent(query))
|
|
350
|
+
.then(res => res.json())
|
|
351
|
+
.then(data => {
|
|
352
|
+
if (data && data.ok) {
|
|
353
|
+
setTasks(data.tasks || []);
|
|
354
|
+
setRecs(data.recommendations || []);
|
|
355
|
+
setStats(data.stats || null);
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
.catch(() => {});
|
|
359
|
+
}, 8000);
|
|
360
|
+
return () => clearInterval(pollInterval);
|
|
361
|
+
}, [tab, query]);
|
|
346
362
|
React.useEffect(() => { loadModels(); }, []);
|
|
347
363
|
|
|
348
364
|
const handleToggleTask = async (id) => {
|
|
@@ -354,10 +370,12 @@ const React = window.React || require('react');
|
|
|
354
370
|
|
|
355
371
|
const handleRunNow = async (id) => {
|
|
356
372
|
try {
|
|
373
|
+
setTasks(prev => prev.map(t => t.id === id ? { ...t, lastStatus: 'running' } : t));
|
|
357
374
|
await fetch('/dsh-cron/tasks/' + encodeURIComponent(id) + '/run', { method: 'POST' });
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
375
|
+
setTimeout(() => fetchTasks(), 1500);
|
|
376
|
+
} catch (err) {
|
|
377
|
+
console.error('[dsh-cron] run now error:', err);
|
|
378
|
+
}
|
|
361
379
|
};
|
|
362
380
|
|
|
363
381
|
const handleDelete = async (id) => {
|
package/lib/runner.js
CHANGED
|
@@ -50,9 +50,13 @@ export class SessionRunner {
|
|
|
50
50
|
if (task.type === 'script' || task.mode === 'no-llm') {
|
|
51
51
|
const { exec } = await import('node:child_process');
|
|
52
52
|
return new Promise((resolve, reject) => {
|
|
53
|
+
const shellPath = process.platform === 'win32' ? undefined : (process.env.SHELL || '/bin/bash');
|
|
53
54
|
const child = exec(task.prompt, {
|
|
54
55
|
cwd: process.cwd(),
|
|
55
56
|
signal,
|
|
57
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
58
|
+
shell: shellPath,
|
|
59
|
+
env: { ...process.env },
|
|
56
60
|
}, (err, stdout, stderr) => {
|
|
57
61
|
if (err) {
|
|
58
62
|
if (signal && signal.aborted) {
|
|
@@ -100,11 +104,13 @@ export class SessionRunner {
|
|
|
100
104
|
const model = task.model || defaultSel?.model || 'deepseek-v4-flash';
|
|
101
105
|
|
|
102
106
|
const cwd = process.cwd();
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
let handle = null;
|
|
108
|
+
try {
|
|
109
|
+
handle = await this.ctx.agents.create({
|
|
110
|
+
sessionId: mintSessionId(),
|
|
111
|
+
meta: { cwd, ephemeral: true, internal: true },
|
|
112
|
+
agentOptions: provider && model ? { provider, model } : undefined,
|
|
113
|
+
});
|
|
108
114
|
|
|
109
115
|
if (signal && signal.aborted) {
|
|
110
116
|
throw (signal.reason || new Error('Aborted'));
|
|
@@ -141,6 +147,17 @@ export class SessionRunner {
|
|
|
141
147
|
usage,
|
|
142
148
|
costUsd
|
|
143
149
|
};
|
|
150
|
+
} finally {
|
|
151
|
+
try {
|
|
152
|
+
if (handle && typeof handle.dispose === 'function') {
|
|
153
|
+
await handle.dispose();
|
|
154
|
+
} else if (handle && handle.agent && typeof handle.agent.destroy === 'function') {
|
|
155
|
+
await handle.agent.destroy();
|
|
156
|
+
}
|
|
157
|
+
} catch (disposeErr) {
|
|
158
|
+
console.warn('[dsh-cron] session dispose notice:', disposeErr.message);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
144
161
|
} catch (err) {
|
|
145
162
|
console.error(`[dsh-cron] agent execution failed:`, err.message);
|
|
146
163
|
throw err;
|
package/lib/scheduler.js
CHANGED
|
@@ -135,8 +135,23 @@ export class TaskScheduler {
|
|
|
135
135
|
|
|
136
136
|
start() {
|
|
137
137
|
const tasks = this.store.list();
|
|
138
|
+
const now = Date.now();
|
|
138
139
|
for (const task of tasks) {
|
|
139
140
|
if (task.status === 'active') {
|
|
141
|
+
// Detect missed execution if daemon was offline (threshold: > 5 minutes)
|
|
142
|
+
if (task.nextRunAt && (now - task.nextRunAt) > 5 * 60 * 1000) {
|
|
143
|
+
const missedAt = task.nextRunAt;
|
|
144
|
+
console.warn(`[dsh-cron] Task "${task.title}" (${task.id}) missed scheduled run at ${new Date(missedAt).toISOString()}`);
|
|
145
|
+
this.store.recordRun(task.id, {
|
|
146
|
+
at: missedAt,
|
|
147
|
+
status: 'missed',
|
|
148
|
+
durationMs: 0,
|
|
149
|
+
output: `Запуск пропущен: сервис был выключен или перезагружался (плановый запуск: ${new Date(missedAt).toLocaleString('ru-RU')})`,
|
|
150
|
+
error: null,
|
|
151
|
+
usage: { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0 },
|
|
152
|
+
costUsd: 0,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
140
155
|
this.scheduleTask(task);
|
|
141
156
|
}
|
|
142
157
|
}
|
package/lib/store.js
CHANGED
|
@@ -13,6 +13,7 @@ export class TaskStore {
|
|
|
13
13
|
this.tasks = new Map();
|
|
14
14
|
this.history = new Map();
|
|
15
15
|
this.settings = {};
|
|
16
|
+
this._savePromise = Promise.resolve();
|
|
16
17
|
this.init();
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -57,7 +58,9 @@ export class TaskStore {
|
|
|
57
58
|
history: Object.fromEntries(this.history.entries()),
|
|
58
59
|
settings: this.settings || {},
|
|
59
60
|
};
|
|
60
|
-
|
|
61
|
+
// Unique tmp file per save call with process PID and random token to eliminate file collision
|
|
62
|
+
const rand = Math.random().toString(36).slice(2, 8);
|
|
63
|
+
const tmp = `${this.filePath}.tmp.${process.pid}_${Date.now()}_${rand}`;
|
|
61
64
|
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
|
|
62
65
|
fs.renameSync(tmp, this.filePath);
|
|
63
66
|
} catch (err) {
|