@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.
@@ -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(() => { 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]);
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
- alert('Задача запущена');
359
- fetchTasks();
360
- } catch (err) {}
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
- 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.23",
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",