@goodandready/dsh-cron 0.1.17 → 0.1.19

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/README.md CHANGED
@@ -192,3 +192,8 @@ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
192
192
  - **Automatic Kanban Card Creation**: Automatically creates task/bug cards in `dsh-kanban` on task failure (`on_failure`) or every run (`always`).
193
193
  - **Token & Execution Cost Meter**: Tracks token consumption (input, output, cache tokens) for LLM executions and estimates USD expenses using current model pricing.
194
194
  - **Aggregated Analytics Bar**: Live dashboard displaying active jobs count, total executions, total token consumption, and aggregate estimated dollar spend.
195
+
196
+ ### One-Shot Delayed Tasks (v0.1.18)
197
+ - **Precise Timing & Relative Delays**: Supports one-time tasks triggered at exact ISO 8601 timestamps (`at: 2026-09-05T12:00:00Z`) or human-friendly relative delays (`in 20m`, `in 2h`, `через 15 минут`).
198
+ - **Auto-Completion Lifecycle**: One-shot jobs transition automatically to `completed` status after their single run, preventing unexpected repeats.
199
+ - **Dedicated Filter**: View historical and pending one-shot executions under the «Completed» (`completed`) tab.
package/docs/README.ru.md CHANGED
@@ -175,3 +175,8 @@ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
175
175
  - **Автосоздание карточек в Kanban**: Автоматическое заведение задач в бэклог `dsh-kanban` при обнаружении сбоев/ошибок (`on_failure`) или при каждом завершении (`always`).
176
176
  - **Трекинг токенов и расчет расходов (USD)**: Подсчет потребления токенов (input, output, cacheRead) для каждой задачи и запуск с расчетом примерной стоимости в долларах США.
177
177
  - **Сводная панель статистики**: Отображение общего числа запусков, активных задач, суммарного расхода токенов и совокупных затрат прямо в заголовке планировщика.
178
+
179
+ ### Разовые отложенные задачи (v0.1.18)
180
+ - **Точное время и относительные задержки**: Поддержка однократных задач, запускаемых в конкретный момент времени по стандарту ISO 8601 (`at: 2026-09-05T12:00:00Z`) или через заданный интервал (`in 20m`, `in 2h`, `через 15 минут`).
181
+ - **Автоматический перевод в «Завершенные»**: После единичного выполнения задача автоматически переходит в статус `completed`, исключая повторные старты.
182
+ - **Индикация в интерфейсе**: Разовые задачи помечаются специальным бейджем «Разовая» и фильтруются по вкладке «Завершенные».
package/lib/client.js CHANGED
@@ -145,6 +145,7 @@ const React = window.React || require('react');
145
145
  .dsh-cron-stat-card { background: rgba(255, 255, 255, 0.02); border: 1px solid #222; border-radius: 8px; padding: 10px 14px; }
146
146
  .dsh-cron-stat-val { font-size: 16px; font-weight: 600; color: #fff; }
147
147
  .dsh-cron-stat-lbl { font-size: 11.5px; color: #777; margin-top: 2px; }
148
+ .dsh-cron-oneshot-tag { font-size: 11px; padding: 2px 6px; border-radius: 4px; background: rgba(168, 85, 247, 0.15); color: #c084fc; font-weight: 500; }
148
149
  .dsh-cron-cost-tag { font-size: 11px; padding: 2px 6px; border-radius: 4px; background: rgba(16, 185, 129, 0.12); color: #10b981; font-weight: 500; }
149
150
  .dsh-cron-tokens-tag { font-size: 11px; padding: 2px 6px; border-radius: 4px; background: rgba(59, 130, 246, 0.12); color: #60a5fa; }
150
151
  .dsh-cron-checkbox-row { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #ccc; cursor: pointer; user-select: none; }
@@ -654,6 +655,7 @@ const React = window.React || require('react');
654
655
  }
655
656
  }, task.type === 'script' ? 'Shell' : 'LLM'),
656
657
  task.model ? React.createElement('span', { className: 'dsh-cron-task-model-tag' }, task.model.split('/').pop()) : null,
658
+ task.oneShot ? React.createElement('span', { className: 'dsh-cron-oneshot-tag' }, 'Разовая') : null,
657
659
  task.totalCostUsd > 0 ? React.createElement('span', { className: 'dsh-cron-cost-tag' }, '$' + task.totalCostUsd.toFixed(4)) : null,
658
660
  task.totalTokens > 0 ? React.createElement('span', { className: 'dsh-cron-tokens-tag' }, (task.totalTokens > 1000 ? Math.round(task.totalTokens / 1000) + 'k' : task.totalTokens) + ' tok') : null
659
661
  ),
@@ -815,7 +817,7 @@ const React = window.React || require('react');
815
817
  React.createElement('input', {
816
818
  required: true,
817
819
  value: formSchedule,
818
- placeholder: '0 */6 * * * или every 2h',
820
+ placeholder: '0 */6 * * * или every 2h или in 30m или at: 2026-09-05T15:00:00Z',
819
821
  onChange: (e) => setFormSchedule(e.target.value)
820
822
  })
821
823
  )
@@ -1199,8 +1201,12 @@ const React = window.React || require('react');
1199
1201
  container.setAttribute(VIEW_ATTR, '');
1200
1202
  container.setAttribute('data-dsh-plugin', 'cron');
1201
1203
  column.appendChild(container);
1202
- root = ReactDOM.createRoot(container);
1203
- root.render(React.createElement(CronScreen, { ctx, toggle, onClose: () => toggle.set(false) }));
1204
+ if (ReactDOM && typeof ReactDOM.createRoot === 'function') {
1205
+ root = ReactDOM.createRoot(container);
1206
+ root.render(React.createElement(CronScreen, { ctx, toggle, onClose: () => toggle.set(false) }));
1207
+ } else if (ReactDOM && typeof ReactDOM.render === 'function') {
1208
+ ReactDOM.render(React.createElement(CronScreen, { ctx, toggle, onClose: () => toggle.set(false) }), container);
1209
+ }
1204
1210
  };
1205
1211
 
1206
1212
  const applyActive = () => {
package/lib/index.js CHANGED
@@ -101,7 +101,7 @@ export function apply(ctx, config) {
101
101
  const task = store.set({
102
102
  id: body.id,
103
103
  title: body.title,
104
- schedule: parsed.cronPattern,
104
+ schedule: parsed.cronPattern || body.schedule,
105
105
  scheduleText: body.scheduleText || parsed.humanText,
106
106
  prompt: body.prompt,
107
107
  type: body.type || 'prompt',
@@ -114,6 +114,7 @@ export function apply(ctx, config) {
114
114
  timeoutSeconds: body.timeoutSeconds !== undefined ? Number(body.timeoutSeconds) : 1800,
115
115
  overlapPolicy: body.overlapPolicy || 'skip',
116
116
  kanbanMode: body.kanbanMode || 'none',
117
+ oneShot: Boolean(parsed.isOneShot || body.oneShot),
117
118
  });
118
119
  scheduler.scheduleTask(task);
119
120
  sendJson(res, 200, { ok: true, task });
@@ -316,30 +317,38 @@ export function apply(ctx, config) {
316
317
  // 2. Tools
317
318
  ctx.tools.register(defineTool({
318
319
  name: 'cron_create_task',
319
- description: 'Создать новую запланированную задачу cron (запуск по расписанию)',
320
+ description: 'Создать новую запланированную задачу cron (запуск по расписанию или разово через "at: ISO" или "in 20m")',
320
321
  parameters: {
321
- type: 'object',
322
- properties: {
323
- title: { type: 'string', description: 'Краткое название задачи' },
324
- schedule: { type: 'string', description: 'Расписание: cron-выражение (например, "0 9 * * *") или интервал ("every 2h", "каждый день в 15:00")' },
325
- prompt: { type: 'string', description: 'Промпт/инструкция для агента при запуске' },
326
- type: { type: 'string', enum: ['prompt', 'script'], description: 'Тип задачи' },
327
- delivery: { type: 'string', enum: ['current', 'isolated'], description: 'Режим запуска: current (в текущем чате) или isolated (изолированная сессия)' },
328
- provider: { type: 'string', description: 'Провайдер модели (опционально)' },
329
- model: { type: 'string', description: 'Модель (опционально)' },
330
- notifyTelegram: { type: 'boolean', description: 'Отправлять отчет в Telegram' },
331
- onlyOnFailure: { type: 'boolean', description: 'Отправлять отчет только при сбоях' },
332
- timeoutSeconds: { type: 'number', description: 'Лимит времени выполнения в секундах' },
333
- overlapPolicy: { type: 'string', enum: ['skip', 'queue', 'replace'], description: 'Политика перекрытия' },
334
- kanbanMode: { type: 'string', enum: ['none', 'on_failure', 'always'], description: 'Создавать карточку в Kanban' },
322
+ title: { type: 'string', description: 'Краткое название задачи', required: true },
323
+ schedule: { type: 'string', description: 'Расписание: cron ("0 9 * * *"), интервал ("every 2h") или разово ("in 30m", "at: 2026-09-05T15:00:00Z")', required: true },
324
+ prompt: { type: 'string', description: 'Промпт/инструкция для агента при запуске', required: true },
325
+ type: { type: 'string', enum: ['prompt', 'script'], description: 'Тип задачи' },
326
+ delivery: { type: 'string', enum: ['current', 'isolated'], description: 'Режим запуска: current (в текущем чате) или isolated (изолированная сессия)' },
327
+ provider: { type: 'string', description: 'Провайдер модели (опционально)' },
328
+ model: { type: 'string', description: 'Модель (опционально)' },
329
+ notifyTelegram: { type: 'boolean', description: 'Отправлять отчет в Telegram' },
330
+ onlyOnFailure: { type: 'boolean', description: 'Отправлять отчет только при сбоях' },
331
+ timeoutSeconds: { type: 'number', description: 'Лимит времени выполнения в секундах' },
332
+ overlapPolicy: { type: 'string', enum: ['skip', 'queue', 'replace'], description: 'Политика перекрытия' },
333
+ kanbanMode: { type: 'string', enum: ['none', 'on_failure', 'always'], description: 'Создавать карточку в Kanban' },
334
+ },
335
+ output: {
336
+ schema: {
337
+ type: 'object',
338
+ additionalProperties: true,
339
+ properties: {
340
+ success: { type: 'boolean' },
341
+ message: { type: 'string' },
342
+ task: { type: 'object', additionalProperties: true }
343
+ }
335
344
  },
336
- required: ['title', 'schedule', 'prompt']
345
+ render: (_args, val) => [{ type: 'text', text: val.message || JSON.stringify(val) }]
337
346
  },
338
347
  execute: async (args) => {
339
348
  const parsed = parseScheduleExpression(args.schedule);
340
349
  const task = store.set({
341
350
  title: args.title,
342
- schedule: parsed.cronPattern,
351
+ schedule: parsed.cronPattern || args.schedule,
343
352
  scheduleText: parsed.humanText,
344
353
  prompt: args.prompt,
345
354
  type: args.type || 'prompt',
@@ -352,6 +361,7 @@ export function apply(ctx, config) {
352
361
  timeoutSeconds: Number(args.timeoutSeconds) || 1800,
353
362
  overlapPolicy: args.overlapPolicy || 'skip',
354
363
  kanbanMode: args.kanbanMode || 'none',
364
+ oneShot: Boolean(parsed.isOneShot),
355
365
  });
356
366
  scheduler.scheduleTask(task);
357
367
  return {
@@ -366,10 +376,18 @@ export function apply(ctx, config) {
366
376
  name: 'cron_list_tasks',
367
377
  description: 'Получить список всех запланированных задач cron',
368
378
  parameters: {
369
- type: 'object',
370
- properties: {
371
- status: { type: 'string', enum: ['all', 'active', 'paused'], description: 'Фильтр по статусу' }
372
- }
379
+ status: { type: 'string', enum: ['all', 'active', 'paused', 'completed'], description: 'Фильтр по статусу' }
380
+ },
381
+ output: {
382
+ schema: {
383
+ type: 'object',
384
+ additionalProperties: true,
385
+ properties: {
386
+ count: { type: 'number' },
387
+ tasks: { type: 'array', items: { type: 'object', additionalProperties: true } }
388
+ }
389
+ },
390
+ render: (_args, val) => [{ type: 'text', text: `Всего задач: ${val.count}` }]
373
391
  },
374
392
  execute: async (args) => {
375
393
  const tasks = store.list({ status: args.status || 'all' });
@@ -384,6 +402,7 @@ export function apply(ctx, config) {
384
402
  lastStatus: t.lastStatus,
385
403
  totalTokens: t.totalTokens || 0,
386
404
  totalCostUsd: t.totalCostUsd || 0,
405
+ oneShot: Boolean(t.oneShot),
387
406
  }))
388
407
  };
389
408
  }
@@ -393,11 +412,18 @@ export function apply(ctx, config) {
393
412
  name: 'cron_pause_task',
394
413
  description: 'Приостановить выполнение задачи по её ID',
395
414
  parameters: {
396
- type: 'object',
397
- properties: {
398
- id: { type: 'string', description: 'Идентификатор задачи' }
415
+ id: { type: 'string', description: 'Идентификатор задачи', required: true }
416
+ },
417
+ output: {
418
+ schema: {
419
+ type: 'object',
420
+ additionalProperties: true,
421
+ properties: {
422
+ success: { type: 'boolean' },
423
+ message: { type: 'string' }
424
+ }
399
425
  },
400
- required: ['id']
426
+ render: (_args, val) => [{ type: 'text', text: val.message }]
401
427
  },
402
428
  execute: async (args) => {
403
429
  const task = scheduler.pauseTask(args.id);
@@ -410,11 +436,18 @@ export function apply(ctx, config) {
410
436
  name: 'cron_resume_task',
411
437
  description: 'Возобновить выполнение приостановленной задачи',
412
438
  parameters: {
413
- type: 'object',
414
- properties: {
415
- id: { type: 'string', description: 'Идентификатор задачи' }
439
+ id: { type: 'string', description: 'Идентификатор задачи', required: true }
440
+ },
441
+ output: {
442
+ schema: {
443
+ type: 'object',
444
+ additionalProperties: true,
445
+ properties: {
446
+ success: { type: 'boolean' },
447
+ message: { type: 'string' }
448
+ }
416
449
  },
417
- required: ['id']
450
+ render: (_args, val) => [{ type: 'text', text: val.message }]
418
451
  },
419
452
  execute: async (args) => {
420
453
  const task = scheduler.resumeTask(args.id);
@@ -427,11 +460,18 @@ export function apply(ctx, config) {
427
460
  name: 'cron_delete_task',
428
461
  description: 'Удалить задачу по её ID',
429
462
  parameters: {
430
- type: 'object',
431
- properties: {
432
- id: { type: 'string', description: 'Идентификатор задачи' }
463
+ id: { type: 'string', description: 'Идентификатор задачи', required: true }
464
+ },
465
+ output: {
466
+ schema: {
467
+ type: 'object',
468
+ additionalProperties: true,
469
+ properties: {
470
+ success: { type: 'boolean' },
471
+ message: { type: 'string' }
472
+ }
433
473
  },
434
- required: ['id']
474
+ render: (_args, val) => [{ type: 'text', text: val.message }]
435
475
  },
436
476
  execute: async (args) => {
437
477
  scheduler.pauseTask(args.id);
@@ -444,11 +484,18 @@ export function apply(ctx, config) {
444
484
  name: 'cron_run_task',
445
485
  description: 'Запустить задачу немедленно вне очереди',
446
486
  parameters: {
447
- type: 'object',
448
- properties: {
449
- id: { type: 'string', description: 'Идентификатор задачи' }
487
+ id: { type: 'string', description: 'Идентификатор задачи', required: true }
488
+ },
489
+ output: {
490
+ schema: {
491
+ type: 'object',
492
+ additionalProperties: true,
493
+ properties: {
494
+ success: { type: 'boolean' },
495
+ message: { type: 'string' }
496
+ }
450
497
  },
451
- required: ['id']
498
+ render: (_args, val) => [{ type: 'text', text: val.message }]
452
499
  },
453
500
  execute: async (args) => {
454
501
  const task = store.get(args.id);
package/lib/scheduler.js CHANGED
@@ -28,6 +28,49 @@ export function parseScheduleExpression(input) {
28
28
  const str = String(input || '').trim();
29
29
  if (!str) throw new Error('Строка расписания не должна быть пустой');
30
30
 
31
+ // 1. One-shot ISO 8601 timestamp or explicit "at: <ISO/datetime>"
32
+ const atPrefixMatch = str.match(/^(?:at:\s*|at\s+)(.+)$/i);
33
+ const candidateIso = atPrefixMatch ? atPrefixMatch[1].trim() : str;
34
+ const parsedDate = new Date(candidateIso);
35
+ if (!isNaN(parsedDate.getTime()) && (candidateIso.includes('-') || candidateIso.includes('T') || atPrefixMatch)) {
36
+ const targetMs = parsedDate.getTime();
37
+ return {
38
+ cronPattern: null,
39
+ isOneShot: true,
40
+ targetTimestamp: targetMs,
41
+ humanText: `Разово: ${parsedDate.toLocaleString('ru-RU')}`,
42
+ nextRun: targetMs
43
+ };
44
+ }
45
+
46
+ // 2. Relative one-shot: "in 20m", "in 2h", "in 30s", "in 1d", "через 15 минут"
47
+ const inRelMatch = str.match(/^(?:in\s+|через\s+)(\d+)\s*(s|sec|seconds?|m|min|minutes?|h|hr|hours?|d|days?|мин|минут|часа?|часов)?$/i);
48
+ if (inRelMatch) {
49
+ const num = parseInt(inRelMatch[1], 10);
50
+ const unit = (inRelMatch[2] || 'm').toLowerCase();
51
+ let delayMs = num * 60 * 1000;
52
+ let unitText = `${num} мин`;
53
+ if (unit.startsWith('s')) {
54
+ delayMs = num * 1000;
55
+ unitText = `${num} сек`;
56
+ } else if (unit.startsWith('h') || unit.startsWith('час')) {
57
+ delayMs = num * 3600 * 1000;
58
+ unitText = `${num} ч`;
59
+ } else if (unit.startsWith('d')) {
60
+ delayMs = num * 86400 * 1000;
61
+ unitText = `${num} дн`;
62
+ }
63
+ const targetMs = Date.now() + delayMs;
64
+ return {
65
+ cronPattern: null,
66
+ isOneShot: true,
67
+ targetTimestamp: targetMs,
68
+ humanText: `Разово через ${unitText} (${new Date(targetMs).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })})`,
69
+ nextRun: targetMs
70
+ };
71
+ }
72
+
73
+ // 3. Repeating Interval: "every 5m", "every 2h", "every 1d"
31
74
  const intervalMatch = str.match(/^every\s+(\d+)\s*(m|min|minute|minutes|h|hr|hour|hours|d|day|days)?$/i);
32
75
  if (intervalMatch) {
33
76
  const num = parseInt(intervalMatch[1], 10);
@@ -55,6 +98,7 @@ export function parseScheduleExpression(input) {
55
98
  }
56
99
  }
57
100
 
101
+ // 4. Friendly recurring aliases
58
102
  const lower = str.toLowerCase();
59
103
  if (lower === 'daily' || lower === 'каждый день') {
60
104
  return { cronPattern: '0 9 * * *', humanText: 'Каждый день в 09:00' };
@@ -66,6 +110,7 @@ export function parseScheduleExpression(input) {
66
110
  return { cronPattern: '0 * * * *', humanText: 'Каждый час' };
67
111
  }
68
112
 
113
+ // 5. Standard 5-field cron expression
69
114
  try {
70
115
  const testJob = new Cron(str);
71
116
  const next = testJob.nextRun();
@@ -83,7 +128,8 @@ export class TaskScheduler {
83
128
  constructor(store, executeFn) {
84
129
  this.store = store;
85
130
  this.executeFn = executeFn;
86
- this.jobs = new Map();
131
+ this.jobs = new Map(); // taskId -> Cron instance
132
+ this.timers = new Map(); // taskId -> setTimeout handle (for one-shot tasks)
87
133
  this.running = new Map(); // taskId -> { controller, startedAt, queueCount }
88
134
  }
89
135
 
@@ -104,6 +150,13 @@ export class TaskScheduler {
104
150
  }
105
151
  this.jobs.clear();
106
152
 
153
+ for (const [id, timer] of this.timers.entries()) {
154
+ try {
155
+ clearTimeout(timer);
156
+ } catch {}
157
+ }
158
+ this.timers.clear();
159
+
107
160
  for (const [, run] of this.running.entries()) {
108
161
  try {
109
162
  run.controller.abort(new Error('Планировщик остановлен'));
@@ -113,15 +166,40 @@ export class TaskScheduler {
113
166
  }
114
167
 
115
168
  scheduleTask(task) {
169
+ // Clear any existing job or timer for this task
116
170
  if (this.jobs.has(task.id)) {
117
171
  this.jobs.get(task.id).stop();
118
172
  this.jobs.delete(task.id);
119
173
  }
174
+ if (this.timers.has(task.id)) {
175
+ clearTimeout(this.timers.get(task.id));
176
+ this.timers.delete(task.id);
177
+ }
120
178
 
121
179
  if (task.status !== 'active') return;
122
180
 
123
181
  try {
124
182
  const parsed = parseScheduleExpression(task.schedule);
183
+
184
+ // Handle One-shot task
185
+ if (parsed.isOneShot) {
186
+ task.oneShot = true;
187
+ const now = Date.now();
188
+ const delay = Math.max(0, parsed.targetTimestamp - now);
189
+ task.nextRunAt = parsed.targetTimestamp;
190
+ this.store.set(task);
191
+
192
+ // If target is in the past, or when delay triggers:
193
+ const timer = setTimeout(async () => {
194
+ this.timers.delete(task.id);
195
+ await this.runTask(task.id);
196
+ }, delay);
197
+
198
+ this.timers.set(task.id, timer);
199
+ return;
200
+ }
201
+
202
+ // Handle Recurring Cron job
125
203
  const job = new Cron(parsed.cronPattern, { protect: true }, async () => {
126
204
  await this.runTask(task.id);
127
205
  });
@@ -271,11 +349,18 @@ export class TaskScheduler {
271
349
  console.error('[dsh-cron] kanban error:', kanbanErr.message);
272
350
  }
273
351
 
274
- const job = this.jobs.get(taskId);
275
- if (job) {
276
- const next = job.nextRun();
277
- task.nextRunAt = next ? next.getTime() : null;
352
+ // 3. Reschedule or mark completed for one-shot
353
+ if (task.oneShot) {
354
+ task.status = 'completed';
355
+ task.nextRunAt = null;
278
356
  this.store.set(task);
357
+ } else {
358
+ const job = this.jobs.get(taskId);
359
+ if (job) {
360
+ const next = job.nextRun();
361
+ task.nextRunAt = next ? next.getTime() : null;
362
+ this.store.set(task);
363
+ }
279
364
  }
280
365
 
281
366
  if (queuedCount > 0) {
@@ -295,6 +380,10 @@ export class TaskScheduler {
295
380
  this.jobs.get(taskId).stop();
296
381
  this.jobs.delete(taskId);
297
382
  }
383
+ if (this.timers.has(taskId)) {
384
+ clearTimeout(this.timers.get(taskId));
385
+ this.timers.delete(taskId);
386
+ }
298
387
  const running = this.running.get(taskId);
299
388
  if (running) {
300
389
  try {
@@ -320,6 +409,10 @@ export class TaskScheduler {
320
409
  this.jobs.get(taskId).stop();
321
410
  this.jobs.delete(taskId);
322
411
  }
412
+ if (this.timers.has(taskId)) {
413
+ clearTimeout(this.timers.get(taskId));
414
+ this.timers.delete(taskId);
415
+ }
323
416
  const running = this.running.get(taskId);
324
417
  if (running) {
325
418
  try {
@@ -337,6 +430,9 @@ export class TaskScheduler {
337
430
  getNextRun(schedule) {
338
431
  try {
339
432
  const parsed = parseScheduleExpression(schedule);
433
+ if (parsed.isOneShot) {
434
+ return parsed.targetTimestamp;
435
+ }
340
436
  const tempJob = new Cron(parsed.cronPattern);
341
437
  const next = tempJob.nextRun();
342
438
  return next ? next.getTime() : null;
package/lib/store.js CHANGED
@@ -132,6 +132,7 @@ export class TaskStore {
132
132
  timeoutSeconds: task.timeoutSeconds !== undefined ? Number(task.timeoutSeconds) : (prev.timeoutSeconds || 1800),
133
133
  overlapPolicy: task.overlapPolicy !== undefined ? String(task.overlapPolicy) : (prev.overlapPolicy || 'skip'),
134
134
  kanbanMode: task.kanbanMode !== undefined ? String(task.kanbanMode) : (prev.kanbanMode || 'none'), // 'none' | 'on_failure' | 'always'
135
+ oneShot: task.oneShot !== undefined ? Boolean(task.oneShot) : Boolean(prev.oneShot),
135
136
  totalTokens: prev.totalTokens || 0,
136
137
  totalCostUsd: prev.totalCostUsd || 0,
137
138
  createdAt: prev.createdAt || task.createdAt || now,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-cron",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Scheduled cron tasks, background automation and agent execution for DeepSeek Harness.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",