@goodandready/dsh-cron 0.1.22 → 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.
@@ -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
@@ -239,6 +239,8 @@ const React = window.React || require('react');
239
239
  const [query, setQuery] = React.useState('');
240
240
  const [tasks, setTasks] = React.useState([]);
241
241
  const [recs, setRecs] = React.useState([]);
242
+ const [loading, setLoading] = React.useState(false);
243
+ const [fetchError, setFetchError] = React.useState(null);
242
244
  const [dropdownOpen, setDropdownOpen] = React.useState(false);
243
245
 
244
246
  // Manual / Edit modal state
@@ -300,16 +302,20 @@ const React = window.React || require('react');
300
302
 
301
303
  const fetchTasks = async () => {
302
304
  setLoading(true);
305
+ setFetchError(null);
303
306
  try {
304
307
  const res = await fetch('/dsh-cron/tasks?status=' + encodeURIComponent(tab) + '&query=' + encodeURIComponent(query));
305
308
  const data = await res.json();
306
309
  if (data && data.ok) {
307
310
  setTasks(data.tasks || []);
308
- setRecommendations(data.recommendations || []);
311
+ setRecs(data.recommendations || []);
309
312
  setStats(data.stats || null);
313
+ } else {
314
+ setFetchError(data && data.error ? data.error : 'Не удалось загрузить задачи');
310
315
  }
311
316
  } catch (err) {
312
317
  console.error('[dsh-cron] fetch tasks error:', err);
318
+ setFetchError(err.message || 'Ошибка сети при загрузке задач');
313
319
  } finally {
314
320
  setLoading(false);
315
321
  }
@@ -336,7 +342,23 @@ const React = window.React || require('react');
336
342
  }
337
343
  };
338
344
 
339
- React.useEffect(() => { fetchTasks(); }, [tab, query]);
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]);
340
362
  React.useEffect(() => { loadModels(); }, []);
341
363
 
342
364
  const handleToggleTask = async (id) => {
@@ -348,10 +370,12 @@ const React = window.React || require('react');
348
370
 
349
371
  const handleRunNow = async (id) => {
350
372
  try {
373
+ setTasks(prev => prev.map(t => t.id === id ? { ...t, lastStatus: 'running' } : t));
351
374
  await fetch('/dsh-cron/tasks/' + encodeURIComponent(id) + '/run', { method: 'POST' });
352
- alert('Задача запущена');
353
- fetchTasks();
354
- } catch (err) {}
375
+ setTimeout(() => fetchTasks(), 1500);
376
+ } catch (err) {
377
+ console.error('[dsh-cron] run now error:', err);
378
+ }
355
379
  };
356
380
 
357
381
  const handleDelete = async (id) => {
@@ -649,10 +673,25 @@ const React = window.React || require('react');
649
673
  }, t === 'all' ? 'Все' : t === 'active' ? 'Активные' : t === 'paused' ? 'На паузе' : 'Завершенные')
650
674
  )
651
675
  ),
676
+ fetchError ? React.createElement('div', {
677
+ className: 'dsh-cron-status-banner',
678
+ 'data-type': 'error',
679
+ style: { margin: '8px 0', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }
680
+ },
681
+ React.createElement('span', null, 'Ошибка загрузки задач: ' + fetchError),
682
+ React.createElement('button', {
683
+ type: 'button',
684
+ className: 'dsh-cron-btn',
685
+ style: { marginLeft: '8px', padding: '2px 8px', fontSize: '12px' },
686
+ onClick: () => fetchTasks()
687
+ }, 'Повторить')
688
+ ) : null,
652
689
  React.createElement('div', { className: 'dsh-cron-task-list' },
653
- tasks.length === 0
654
- ? React.createElement('div', { style: { color: '#777', padding: '16px 0', fontSize: '14px' } }, 'Нет запланированных задач')
655
- : tasks.map((task) =>
690
+ loading && tasks.length === 0
691
+ ? React.createElement('div', { style: { color: '#888', padding: '16px 0', fontSize: '14px' } }, 'Загрузка запланированных задач...')
692
+ : tasks.length === 0
693
+ ? React.createElement('div', { style: { color: '#777', padding: '16px 0', fontSize: '14px' } }, 'Нет запланированных задач')
694
+ : tasks.map((task) =>
656
695
  React.createElement('div', {
657
696
  key: task.id,
658
697
  className: 'dsh-cron-task-item',
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
- const handle = await this.ctx.agents.create({
104
- sessionId: mintSessionId(),
105
- meta: { cwd },
106
- agentOptions: provider && model ? { provider, model } : undefined,
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
- const tmp = this.filePath + '.tmp.' + Date.now();
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-cron",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "description": "Scheduled cron tasks, background automation and agent execution for DeepSeek Harness.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",