@goodandready/dsh-cron 0.2.12 → 0.2.14

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
@@ -359,6 +359,30 @@ Developer-facing, no behaviour change. `parseScheduleExpression` was split into
359
359
 
360
360
  ---
361
361
 
362
+ ### 25. Persistent Execution Sessions & Context Continuity (`targetSessionId`, Added in v0.2.13, #143)
363
+ - **Continuous Conversation Context**: Tasks can configure a `targetSessionId`. When set, the scheduler resumes the existing session via `agents.resume()` instead of generating an isolated ephemeral session (`cron-exec-${id}-${uuid}`) on every tick. The agent retains conversational memory across runs, allowing periodic auditors or assistants to reference prior findings and outputs directly in context.
364
+ - **Context Window Protection & Session Rotation (`targetSessionReset`)**: To prevent unbounded context growth and token cost explosion over long schedules, tasks can set an automatic rotation policy:
365
+ - `never`: Continues a single session thread indefinitely.
366
+ - `daily`: Automatically rotates the session daily (`<id>-YYYY-MM-DD`).
367
+ - `weekly`: Automatically rotates the session weekly (`<id>-YYYY-Www`).
368
+ - Custom templating: Any `targetSessionId` containing `{{date}}` automatically interpolates today's date (`YYYY-MM-DD`).
369
+ - **Main Chat Visibility**: Persistent sessions are omitted from `sessions.archive()` and unflagged from `ephemeral`/`internal`, making them visible and interactable directly in the DeepSeek Harness chat interface.
370
+ - **Preset & Tooling Compatibility**: Resumed sessions automatically preserve and remount configured `agentPresets`, ensuring full access to workspace, terminal, and file tools on every turn.
371
+ - *Inspiration credit*: Community idea originally explored by [@RaulLazaro](https://github.com/RaulLazaro).
372
+
373
+ ---
374
+
375
+ ### 26. Reliability, Hardening & Self-Healing Maintenance Pack (v0.2.14, #145)
376
+ - **Automatic Retry Budget Reset**: Fixed retry exhaustion amnesia. When a task exhausts its configured retry attempts (`maxRetries`), its attempts counter is automatically cleared, ensuring that subsequent scheduled ticks retain their full retry budget. Any regular or manual execution also guarantees a clean retry budget on launch.
377
+ - **Zombie Queue Elimination**: Tasks paused via UI/API or deleted are immediately purged from the scheduler's concurrency waiting queue (`this.queue`). Additionally, when dequeuing tasks upon concurrency slot release, inactive or deleted tasks are safely skipped.
378
+ - **Bounded History Archive**: Long-running production instances with high-frequency cron tasks safely bound `tasks-history-archive.json` to the latest 1,000 runs per task, eliminating unbounded disk growth and synchronous JSON serialization lag.
379
+ - **Disaster Recovery & Store Auto-Backup**: `TaskStore` maintains an automatic atomic `.bak` copy of `tasks.json` on every successful save. In the event of process crash or file corruption, the store snapshots the corrupted file to `tasks.json.corrupted.<timestamp>` for forensic analysis and self-heals seamlessly from the backup.
380
+ - **Context Window Overflow Auto-Recovery**: For persistent sessions (`targetSessionId`), if an agent turn fails due to context window saturation (`context_length_exceeded`), the runner detects the overflow, archives the exhausted session, automatically rotates to a fresh session thread, and transparently retries execution without task failure.
381
+ - **Windows Process Tree Termination**: External process runners on Windows now execute `taskkill /pid <pid> /T /F` on task cancellation or timeout, preventing zombie background processes and orphan shells from lingering in the operating system.
382
+
383
+ ---
384
+
385
+
362
386
  ## 📦 Installation
363
387
 
364
388
  Install into your DeepSeek Harness web profile:
package/README.ru.md CHANGED
@@ -235,6 +235,30 @@ cron_create_task({
235
235
 
236
236
  ---
237
237
 
238
+ ### 25. Долговременные сессии и непрерывность контекста (`targetSessionId`, добавлено в v0.2.13, #143)
239
+ - **Непрерывный контекст диалога**: Для задач можно задать `targetSessionId`. При наличии этого идентификатора планировщик возобновляет существующую сессию через `agents.resume()` вместо создания одноразовой сессии (`cron-exec-${id}-${uuid}`) на каждом тике. Агент сохраняет память предыдущих ходов и может ссылаться на ранее обнаруженные данные и выводы.
240
+ - **Защита от переполнения контекста и ротация (`targetSessionReset`)**: Чтобы контекстное окно и расход токенов не разрастались бесконечно при частых запусках, предусмотрены политики автоматической ротации:
241
+ - `never`: единая непрерывная сессия без сброса.
242
+ - `daily`: ежедневная автоматическая ротация (`<id>-YYYY-MM-DD`).
243
+ - `weekly`: еженедельная автоматическая ротация (`<id>-YYYY-Www`).
244
+ - Шаблоны дат: в `targetSessionId` поддерживается плейсхолдер `{{date}}`, который автоматически заменяется на текущую дату `YYYY-MM-DD`.
245
+ - **Видимость в списке чатов DSH**: Долговременные сессии не помечаются как `ephemeral`/`internal` и исключены из автоматической архивации (`sessions.archive()`), поэтому они остаются доступны для чтения и прямого диалога в веб-интерфейсе DSH.
246
+ - **Совместимость с пресетами и инструментами**: При возобновлении сессии автоматически подключаются инструменты пресета `agentPreset`, гарантируя доступ к терминалу, файлам и командам.
247
+ - *Благодарность*: концепция вдохновлена разработкой [@RaulLazaro](https://github.com/RaulLazaro).
248
+
249
+ ---
250
+
251
+ ### 26. Пакет надежности, отказоустойчивости и самовосстановления (v0.2.14, #145)
252
+ - **Автоматический сброс бюджета повторов**: Исправлена «амнезия повторов». Когда задача исчерпывает лимит попыток (`maxRetries`), счётчик `attempts` автоматически обнуляется, поэтому следующий плановый запуск по расписанию получает полный бюджет повторов с нуля. Любой регулярный или ручной запуск гарантированно начинает выполнение со сброшенным счётчиком попыток.
253
+ - **Устранение «зомби»-задач в очереди**: Задачи, приостановленные через интерфейс/API или удалённые, мгновенно вычищаются из очереди ожидания параллелизма (`this.queue`). При освобождении слотов очереди неактивные или удалённые задачи безопасно пропускаются.
254
+ - **Ограничение архива истории**: В длительно работающих инсталляциях с высокочастотными cron-задачами файл архива `tasks-history-archive.json` теперь надёжно ограничен последними 1 000 запусками на задачу, предотвращая неконтролируемый рост диска и синхронные задержки сериализации JSON.
255
+ - **Аварийное восстановление и авто-бэкап хранилища**: `TaskStore` автоматически поддерживает атомарную резервную копию `tasks.json.bak` при каждом успешном сохранении. В случае сбоя или повреждения файла хранилище делает снимок `tasks.json.corrupted.<timestamp>` для диагностики и бесшовно восстанавливается из резервной копии.
256
+ - **Самовосстановление при переполнении контекстного окна**: Если в долговременной сессии (`targetSessionId`) очередной ход агента завершается ошибкой переполнения контекста модели (`context_length_exceeded`), раннер распознаёт переполнение, архивирует исчерпанную сессию, автоматически выполняет ротацию на свежую сессию и прозрачно повторяет выполнение без срыва задачи.
257
+ - **Корректное завершение дерева процессов в Windows**: На платформе Windows отмена или таймаут внешних скриптовых задач теперь вызывают `taskkill /pid <pid> /T /F`, гарантируя полное уничтожение всех дочерних процессов и оболочек без зависания зомби-процессов в системе.
258
+
259
+ ---
260
+
261
+
238
262
  ## 📦 Установка
239
263
 
240
264
  ```bash
package/README.zh.md CHANGED
@@ -358,6 +358,30 @@ bash deploy.sh verify [exact-version]
358
358
 
359
359
  ---
360
360
 
361
+ ### 25. 常驻持久会话与上下文延续 (`targetSessionId`,v0.2.13 新增,#143)
362
+ - **跨周期会话上下文延续**:支持在任务中配置 `targetSessionId`。设置后,调度器将在每次定时触发时通过 `agents.resume()` 唤醒已有会话,而不再每次生成孤立的临时会话(`cron-exec-${id}-${uuid}`)。智能体能够完整继承上一轮对话的历史记忆与分析结论。
363
+ - **上下文窗口保护与轮转机制 (`targetSessionReset`)**:为防止高频执行导致模型上下文窗口超限与 Token 成本暴增,支持智能轮转策略:
364
+ - `never`:持续累积单一会话,不重置。
365
+ - `daily`:每日自动开启全新子会话(后缀 `<id>-YYYY-MM-DD`)。
366
+ - `weekly`:每周自动开启全新子会话(后缀 `<id>-YYYY-Www`)。
367
+ - 日期变量插值:`targetSessionId` 中支持 `{{date}}` 占位符,自动注入当前日期 `YYYY-MM-DD`。
368
+ - **主界面原生可见交互**:持久会话不会被标记为 `ephemeral`/`internal`,且在执行后跳过自动归档(`sessions.archive()`),用户可在 DSH 聊天列表中直接查看并继续手动对话。
369
+ - **预设工具链无缝适配**:恢复会话时同样完整挂载 `agentPresets`,保障文件读写、代码编辑与终端工具持续可用。
370
+ - *致谢*:功能灵感源自社区开发者 [@RaulLazaro](https://github.com/RaulLazaro)。
371
+
372
+ ---
373
+
374
+ ### 26. 系统稳定性、硬化与自愈维护包 (v0.2.14, #145)
375
+ - **重试预算自动重置**:彻底修复重试耗尽后的计数残留问题。当任务耗尽配置的重试次数(`maxRetries`)后,`attempts` 计数器自动清零,确保后续周期的定时调度享有完整的重试预算。常规计划执行或手动触发也均保证以干净的重试预算启动。
376
+ - **清除队列僵尸任务**:通过界面/API 暂停或删除任务时,调度器会立即将其从并发等待队列(`this.queue`)中剔除;并发槽位释放出队时,非激活或已删除的任务也会被自动安全跳过。
377
+ - **历史归档容量上限保护**:针对长期运行和高频调度的生产环境,`tasks-history-archive.json` 针对每个任务安全限制保留最新的 1,000 条运行记录,消除无限制磁盘占用与同步 JSON 序列化卡顿。
378
+ - **灾难恢复与存储自动备份**:`TaskStore` 在每次成功持久化时自动维护原子的 `tasks.json.bak` 备份副本。若发生进程异常导致数据损坏,存储引擎会自动保存现场切片 `tasks.json.corrupted.<timestamp>` 供故障分析,并无缝从备份中自愈恢复。
379
+ - **上下文超限自愈与平滑轮转**:在常驻持久会话(`targetSessionId`)中,若智能体因模型上下文窗口溢出(`context_length_exceeded`)失败,运行器将精准捕获超限错误,归档已满会话,自动轮转至全新子会话并平滑重试,避免任务中断。
380
+ - **Windows 进程树彻底终止**:在 Windows 系统上,外部进程任务被取消或超时终止时,改为执行 `taskkill /pid <pid> /T /F`,杜绝孤儿进程和后台残留外壳。
381
+
382
+ ---
383
+
384
+
361
385
  ## 📦 安装
362
386
 
363
387
  ```bash
package/docs/README.ru.md CHANGED
@@ -235,6 +235,30 @@ cron_create_task({
235
235
 
236
236
  ---
237
237
 
238
+ ### 25. Долговременные сессии и непрерывность контекста (`targetSessionId`, добавлено в v0.2.13, #143)
239
+ - **Непрерывный контекст диалога**: Для задач можно задать `targetSessionId`. При наличии этого идентификатора планировщик возобновляет существующую сессию через `agents.resume()` вместо создания одноразовой сессии (`cron-exec-${id}-${uuid}`) на каждом тике. Агент сохраняет память предыдущих ходов и может ссылаться на ранее обнаруженные данные и выводы.
240
+ - **Защита от переполнения контекста и ротация (`targetSessionReset`)**: Чтобы контекстное окно и расход токенов не разрастались бесконечно при частых запусках, предусмотрены политики автоматической ротации:
241
+ - `never`: единая непрерывная сессия без сброса.
242
+ - `daily`: ежедневная автоматическая ротация (`<id>-YYYY-MM-DD`).
243
+ - `weekly`: еженедельная автоматическая ротация (`<id>-YYYY-Www`).
244
+ - Шаблоны дат: в `targetSessionId` поддерживается плейсхолдер `{{date}}`, который автоматически заменяется на текущую дату `YYYY-MM-DD`.
245
+ - **Видимость в списке чатов DSH**: Долговременные сессии не помечаются как `ephemeral`/`internal` и исключены из автоматической архивации (`sessions.archive()`), поэтому они остаются доступны для чтения и прямого диалога в веб-интерфейсе DSH.
246
+ - **Совместимость с пресетами и инструментами**: При возобновлении сессии автоматически подключаются инструменты пресета `agentPreset`, гарантируя доступ к терминалу, файлам и командам.
247
+ - *Благодарность*: концепция вдохновлена разработкой [@RaulLazaro](https://github.com/RaulLazaro).
248
+
249
+ ---
250
+
251
+ ### 26. Пакет надежности, отказоустойчивости и самовосстановления (v0.2.14, #145)
252
+ - **Автоматический сброс бюджета повторов**: Исправлена «амнезия повторов». Когда задача исчерпывает лимит попыток (`maxRetries`), счётчик `attempts` автоматически обнуляется, поэтому следующий плановый запуск по расписанию получает полный бюджет повторов с нуля. Любой регулярный или ручной запуск гарантированно начинает выполнение со сброшенным счётчиком попыток.
253
+ - **Устранение «зомби»-задач в очереди**: Задачи, приостановленные через интерфейс/API или удалённые, мгновенно вычищаются из очереди ожидания параллелизма (`this.queue`). При освобождении слотов очереди неактивные или удалённые задачи безопасно пропускаются.
254
+ - **Ограничение архива истории**: В длительно работающих инсталляциях с высокочастотными cron-задачами файл архива `tasks-history-archive.json` теперь надёжно ограничен последними 1 000 запусками на задачу, предотвращая неконтролируемый рост диска и синхронные задержки сериализации JSON.
255
+ - **Аварийное восстановление и авто-бэкап хранилища**: `TaskStore` автоматически поддерживает атомарную резервную копию `tasks.json.bak` при каждом успешном сохранении. В случае сбоя или повреждения файла хранилище делает снимок `tasks.json.corrupted.<timestamp>` для диагностики и бесшовно восстанавливается из резервной копии.
256
+ - **Самовосстановление при переполнении контекстного окна**: Если в долговременной сессии (`targetSessionId`) очередной ход агента завершается ошибкой переполнения контекста модели (`context_length_exceeded`), раннер распознаёт переполнение, архивирует исчерпанную сессию, автоматически выполняет ротацию на свежую сессию и прозрачно повторяет выполнение без срыва задачи.
257
+ - **Корректное завершение дерева процессов в Windows**: На платформе Windows отмена или таймаут внешних скриптовых задач теперь вызывают `taskkill /pid <pid> /T /F`, гарантируя полное уничтожение всех дочерних процессов и оболочек без зависания зомби-процессов в системе.
258
+
259
+ ---
260
+
261
+
238
262
  ## 📦 Установка
239
263
 
240
264
  ```bash
package/docs/README.zh.md CHANGED
@@ -358,6 +358,30 @@ bash deploy.sh verify [exact-version]
358
358
 
359
359
  ---
360
360
 
361
+ ### 25. 常驻持久会话与上下文延续 (`targetSessionId`,v0.2.13 新增,#143)
362
+ - **跨周期会话上下文延续**:支持在任务中配置 `targetSessionId`。设置后,调度器将在每次定时触发时通过 `agents.resume()` 唤醒已有会话,而不再每次生成孤立的临时会话(`cron-exec-${id}-${uuid}`)。智能体能够完整继承上一轮对话的历史记忆与分析结论。
363
+ - **上下文窗口保护与轮转机制 (`targetSessionReset`)**:为防止高频执行导致模型上下文窗口超限与 Token 成本暴增,支持智能轮转策略:
364
+ - `never`:持续累积单一会话,不重置。
365
+ - `daily`:每日自动开启全新子会话(后缀 `<id>-YYYY-MM-DD`)。
366
+ - `weekly`:每周自动开启全新子会话(后缀 `<id>-YYYY-Www`)。
367
+ - 日期变量插值:`targetSessionId` 中支持 `{{date}}` 占位符,自动注入当前日期 `YYYY-MM-DD`。
368
+ - **主界面原生可见交互**:持久会话不会被标记为 `ephemeral`/`internal`,且在执行后跳过自动归档(`sessions.archive()`),用户可在 DSH 聊天列表中直接查看并继续手动对话。
369
+ - **预设工具链无缝适配**:恢复会话时同样完整挂载 `agentPresets`,保障文件读写、代码编辑与终端工具持续可用。
370
+ - *致谢*:功能灵感源自社区开发者 [@RaulLazaro](https://github.com/RaulLazaro)。
371
+
372
+ ---
373
+
374
+ ### 26. 系统稳定性、硬化与自愈维护包 (v0.2.14, #145)
375
+ - **重试预算自动重置**:彻底修复重试耗尽后的计数残留问题。当任务耗尽配置的重试次数(`maxRetries`)后,`attempts` 计数器自动清零,确保后续周期的定时调度享有完整的重试预算。常规计划执行或手动触发也均保证以干净的重试预算启动。
376
+ - **清除队列僵尸任务**:通过界面/API 暂停或删除任务时,调度器会立即将其从并发等待队列(`this.queue`)中剔除;并发槽位释放出队时,非激活或已删除的任务也会被自动安全跳过。
377
+ - **历史归档容量上限保护**:针对长期运行和高频调度的生产环境,`tasks-history-archive.json` 针对每个任务安全限制保留最新的 1,000 条运行记录,消除无限制磁盘占用与同步 JSON 序列化卡顿。
378
+ - **灾难恢复与存储自动备份**:`TaskStore` 在每次成功持久化时自动维护原子的 `tasks.json.bak` 备份副本。若发生进程异常导致数据损坏,存储引擎会自动保存现场切片 `tasks.json.corrupted.<timestamp>` 供故障分析,并无缝从备份中自愈恢复。
379
+ - **上下文超限自愈与平滑轮转**:在常驻持久会话(`targetSessionId`)中,若智能体因模型上下文窗口溢出(`context_length_exceeded`)失败,运行器将精准捕获超限错误,归档已满会话,自动轮转至全新子会话并平滑重试,避免任务中断。
380
+ - **Windows 进程树彻底终止**:在 Windows 系统上,外部进程任务被取消或超时终止时,改为执行 `taskkill /pid <pid> /T /F`,杜绝孤儿进程和后台残留外壳。
381
+
382
+ ---
383
+
384
+
361
385
  ## 📦 安装
362
386
 
363
387
  ```bash
package/lib/api.js CHANGED
@@ -503,7 +503,11 @@ function deleteTask({ store, scheduler, res, id }) {
503
503
  return;
504
504
  }
505
505
  if (current) {
506
- scheduler.pauseTask(id);
506
+ if (scheduler && typeof scheduler.removeTask === 'function') {
507
+ scheduler.removeTask(id);
508
+ } else if (scheduler) {
509
+ scheduler.pauseTask(id);
510
+ }
507
511
  store.delete(id);
508
512
  }
509
513
  sendJson(res, 200, { ok: true });
package/lib/client.js CHANGED
@@ -326,6 +326,14 @@ window.__ModuleLoader__.load({
326
326
  'form.agentPresetLabel': 'Agent preset',
327
327
  'form.agentPresetPlaceholder': 'standard (leave empty for configured default)',
328
328
  'form.agentPresetHint': 'Agent preset from dsh-agent-presets (e.g. standard, minimal, cordis, ptc). Mounts workspace and coding tools.',
329
+ 'form.targetSessionIdLabel': 'Target persistent session ID',
330
+ 'form.targetSessionIdPlaceholder': 'e.g. audit-chat or dev-summary (empty = isolated run)',
331
+ 'form.targetSessionIdHint': 'When specified, scheduled runs resume this session to maintain conversation context across runs instead of creating a new isolated session.',
332
+ 'form.targetSessionResetLabel': 'Session context rotation',
333
+ 'form.targetSessionResetNever': 'Continuous (never reset)',
334
+ 'form.targetSessionResetDaily': 'Daily rotation (new session each day)',
335
+ 'form.targetSessionResetWeekly': 'Weekly rotation (new session each week)',
336
+ 'form.targetSessionResetHint': 'Automatically rotates the session identifier to prevent context window explosion over long periods.',
329
337
  'form.previewSchedule': 'Preview next runs',
330
338
  'form.previewScheduleTitle': 'Next 5 scheduled runs:',
331
339
  },
@@ -402,6 +410,14 @@ window.__ModuleLoader__.load({
402
410
  'form.agentPresetLabel': '智能体预设 (Agent Preset)',
403
411
  'form.agentPresetPlaceholder': 'standard (留空使用全局默认预设)',
404
412
  'form.agentPresetHint': '来自 dsh-agent-presets 的智能体预设(如 standard, minimal, cordis, ptc),挂载工作区工具与文件读写能力。',
413
+ 'form.targetSessionIdLabel': '目标常驻会话 ID (Target Session ID)',
414
+ 'form.targetSessionIdPlaceholder': '如 audit-chat 或 dev-summary(留空为单次隔离运行)',
415
+ 'form.targetSessionIdHint': '配置后定时任务将在此常驻会话中持续交互,跨周期保留历史上下文对话记忆,不再每次新建孤立会话。',
416
+ 'form.targetSessionResetLabel': '上下文轮转策略 (Session Rotation)',
417
+ 'form.targetSessionResetNever': '永不轮转(单一会话持续累积)',
418
+ 'form.targetSessionResetDaily': '每日轮转(每日自动开启新会话)',
419
+ 'form.targetSessionResetWeekly': '每周轮转(每周自动开启新会话)',
420
+ 'form.targetSessionResetHint': '自动按周期加注日期后缀轮转会话,有效防止长时间运行导致的模型上下文窗口超限与 Token 浪费。',
405
421
  'form.previewSchedule': '预测下次运行时间',
406
422
  'form.previewScheduleTitle': '未来 5 次计划运行时间:',
407
423
  'search.placeholder': '搜索定时任务',
@@ -1162,6 +1178,8 @@ window.__ModuleLoader__.load({
1162
1178
  const [formMaxRetries, setFormMaxRetries] = React.useState(0);
1163
1179
  const [formPermissionPreset, setFormPermissionPreset] = React.useState('default');
1164
1180
  const [formAgentPreset, setFormAgentPreset] = React.useState('');
1181
+ const [formTargetSessionId, setFormTargetSessionId] = React.useState('');
1182
+ const [formTargetSessionReset, setFormTargetSessionReset] = React.useState('never');
1165
1183
  const [formEnv, setFormEnv] = React.useState('');
1166
1184
  const [formCwd, setFormCwd] = React.useState('');
1167
1185
  const [formWorkspaceId, setFormWorkspaceId] = React.useState('');
@@ -1713,6 +1731,8 @@ window.__ModuleLoader__.load({
1713
1731
  setFormMaxRetries(0);
1714
1732
  setFormPermissionPreset('default');
1715
1733
  setFormAgentPreset('');
1734
+ setFormTargetSessionId('');
1735
+ setFormTargetSessionReset('never');
1716
1736
  setFormEnv('');
1717
1737
  setFormCwd('');
1718
1738
  setFormWorkspaceId('');
@@ -1812,6 +1832,8 @@ window.__ModuleLoader__.load({
1812
1832
  setFormMaxRetries(task.maxRetries !== undefined ? task.maxRetries : 0);
1813
1833
  setFormPermissionPreset(task.permissionPreset || 'default');
1814
1834
  setFormAgentPreset(task.agentPreset || '');
1835
+ setFormTargetSessionId(task.targetSessionId || '');
1836
+ setFormTargetSessionReset(task.targetSessionReset || 'never');
1815
1837
  setFormEnv(formatEnvText(task.env));
1816
1838
  setFormCwd(task.cwd || '');
1817
1839
  setFormWorkspaceId(task.workspaceId || '');
@@ -1870,6 +1892,8 @@ window.__ModuleLoader__.load({
1870
1892
  maxRetries: Number(formMaxRetries) || 0,
1871
1893
  permissionPreset: formPermissionPreset,
1872
1894
  agentPreset: formAgentPreset.trim() || undefined,
1895
+ targetSessionId: formTargetSessionId.trim() || undefined,
1896
+ targetSessionReset: formTargetSessionReset || 'never',
1873
1897
  kanbanMode: formKanbanMode,
1874
1898
  onSuccess: formOnSuccess.trim() || undefined,
1875
1899
  onFailure: formOnFailure.trim() || undefined,
@@ -2190,6 +2214,7 @@ window.__ModuleLoader__.load({
2190
2214
  task.onSuccess ? React.createElement('span', { className: 'dsh-cron-type-tag', title: 'On success: triggers ' + task.onSuccess, style: { background: 'rgba(56, 189, 248, 0.15)', color: '#38bdf8' } }, '➜ ' + task.onSuccess) : null,
2191
2215
  task.onFailure ? React.createElement('span', { className: 'dsh-cron-type-tag', title: 'On failure: triggers ' + task.onFailure, style: { background: 'rgba(239, 68, 68, 0.15)', color: '#ef4444' } }, '↳ ' + task.onFailure) : null,
2192
2216
  task.heartbeatIntervalSeconds > 0 ? React.createElement('span', { className: 'dsh-cron-type-tag', title: 'Heartbeat: every ' + task.heartbeatIntervalSeconds + 's', style: { background: 'rgba(168, 85, 247, 0.15)', color: '#a855f7' } }, '💓 ' + task.heartbeatIntervalSeconds + 's') : null,
2217
+ task.targetSessionId ? React.createElement('span', { className: 'dsh-cron-type-tag', title: 'Target session: ' + task.targetSessionId + (task.targetSessionReset && task.targetSessionReset !== 'never' ? ' (' + task.targetSessionReset + ')' : ''), style: { background: 'rgba(99, 102, 241, 0.15)', color: '#818cf8' } }, '🧵 ' + task.targetSessionId) : null,
2193
2218
  task.preflightType && task.preflightType !== 'none' ? React.createElement('span', { className: 'dsh-cron-type-tag', title: 'Preflight: ' + task.preflightType, style: { background: 'rgba(234, 179, 8, 0.15)', color: '#eab308' } }, '🛡️ ' + task.preflightType) : null,
2194
2219
  task.totalCostUsd > 0 ? React.createElement('span', { className: 'dsh-cron-cost-tag' }, '$' + task.totalCostUsd.toFixed(4)) : null,
2195
2220
  task.totalTokens > 0 ? React.createElement('span', { className: 'dsh-cron-tokens-tag' }, (task.totalTokens > 1000 ? Math.round(task.totalTokens / 1000) + 'k' : task.totalTokens) + ' tok') : null
@@ -2540,6 +2565,30 @@ window.__ModuleLoader__.load({
2540
2565
  }),
2541
2566
  React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'form.agentPresetHint'))
2542
2567
  ),
2568
+ AGENT_FORM_TYPES.includes(formType) && React.createElement('div', { className: 'dsh-cron-form-row' },
2569
+ React.createElement('div', { className: 'dsh-cron-form-group' },
2570
+ React.createElement('label', null, T_KEY(t, 'form.targetSessionIdLabel')),
2571
+ React.createElement('input', {
2572
+ type: 'text',
2573
+ value: formTargetSessionId,
2574
+ placeholder: T_KEY(t, 'form.targetSessionIdPlaceholder'),
2575
+ onChange: (e) => setFormTargetSessionId(e.target.value)
2576
+ }),
2577
+ React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'form.targetSessionIdHint'))
2578
+ ),
2579
+ React.createElement('div', { className: 'dsh-cron-form-group' },
2580
+ React.createElement('label', null, T_KEY(t, 'form.targetSessionResetLabel')),
2581
+ React.createElement('select', {
2582
+ value: formTargetSessionReset,
2583
+ onChange: (e) => setFormTargetSessionReset(e.target.value)
2584
+ },
2585
+ React.createElement('option', { value: 'never' }, T_KEY(t, 'form.targetSessionResetNever')),
2586
+ React.createElement('option', { value: 'daily' }, T_KEY(t, 'form.targetSessionResetDaily')),
2587
+ React.createElement('option', { value: 'weekly' }, T_KEY(t, 'form.targetSessionResetWeekly'))
2588
+ ),
2589
+ React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'form.targetSessionResetHint'))
2590
+ )
2591
+ ),
2543
2592
  AGENT_FORM_TYPES.includes(formType) && React.createElement('div', { className: 'dsh-cron-form-row' },
2544
2593
  React.createElement('div', { className: 'dsh-cron-form-group' },
2545
2594
  React.createElement('label', null, T_KEY(t, 'form.providerLabel')),
package/lib/index.js CHANGED
@@ -300,7 +300,7 @@ const createTaskOutput = {
300
300
 
301
301
  export function apply(ctx, config) {
302
302
  const store = new TaskStore();
303
- const runner = new SessionRunner(ctx);
303
+ const runner = new SessionRunner(ctx, store);
304
304
  const scheduler = new TaskScheduler(store, (task, opts) => runner.execute(task, opts), {
305
305
  maxConcurrent: config && config.maxConcurrent,
306
306
  defaultTimezone: config && config.defaultTimezone,
package/lib/runner.js CHANGED
@@ -1,3 +1,41 @@
1
+ /**
2
+ * Helper to detect context window overflow errors in persistent sessions (#145).
3
+ */
4
+ export function isContextOverflowError(err) {
5
+ const msg = String(err?.message || err || '').toLowerCase();
6
+ return msg.includes('context length') ||
7
+ msg.includes('context window') ||
8
+ msg.includes('maximum context') ||
9
+ msg.includes('too many tokens') ||
10
+ msg.includes('prompt too long') ||
11
+ msg.includes('token limit') ||
12
+ msg.includes('context_length_exceeded') ||
13
+ msg.includes('string_above_max_length');
14
+ }
15
+
16
+ /**
17
+ * Cross-platform process tree killer (#145).
18
+ * On Windows, calls `taskkill /pid <pid> /T /F` to terminate the entire process hierarchy.
19
+ */
20
+ export function terminateProcessTree(child, sig = 'SIGTERM', { isPosix = process.platform !== 'win32', execFn } = {}) {
21
+ if (!child || child.killed) return;
22
+ try {
23
+ if (isPosix && child.pid) {
24
+ try { process.kill(-child.pid, sig); } catch {}
25
+ } else if (child.pid) {
26
+ const runner = execFn || ((cmd) => {
27
+ try {
28
+ import('node:child_process').then(({ exec }) => {
29
+ exec(cmd, { windowsHide: true }, () => {});
30
+ }).catch(() => {});
31
+ } catch {}
32
+ });
33
+ try { runner(`taskkill /pid ${child.pid} /T /F`); } catch {}
34
+ try { child.kill(sig); } catch {}
35
+ }
36
+ } catch {}
37
+ }
38
+
1
39
  /**
2
40
  * Helper to test if an error code or status is transient and eligible for retry (#134).
3
41
  */
@@ -23,9 +61,46 @@ import {
23
61
  /**
24
62
  * Runner executing scheduled tasks in DeepSeek Harness sessions.
25
63
  */
64
+
65
+ /**
66
+ * Resolves the effective target session ID taking into account rotation policies (#143).
67
+ * Supports {{date}} interpolation, 'daily' rotation (adds -YYYY-MM-DD), and 'weekly' rotation (-YYYY-Www).
68
+ */
69
+ export function resolveTargetSessionId(targetSessionId, resetPolicy = 'never', date = new Date()) {
70
+ if (!targetSessionId || typeof targetSessionId !== 'string') return null;
71
+ let baseId = targetSessionId.trim();
72
+ if (!baseId) return null;
73
+
74
+ const yyyy = date.getFullYear();
75
+ const mm = String(date.getMonth() + 1).padStart(2, '0');
76
+ const dd = String(date.getDate()).padStart(2, '0');
77
+ const yyyymmdd = `${yyyy}-${mm}-${dd}`;
78
+
79
+ if (baseId.includes('{{date}}')) {
80
+ return baseId.replaceAll('{{date}}', yyyymmdd);
81
+ }
82
+
83
+ if (resetPolicy === 'daily') {
84
+ return `${baseId}-${yyyymmdd}`;
85
+ }
86
+
87
+ if (resetPolicy === 'weekly') {
88
+ const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
89
+ const dayNum = d.getUTCDay() || 7;
90
+ d.setUTCDate(d.getUTCDate() + 4 - dayNum);
91
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
92
+ const weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
93
+ const ww = String(weekNo).padStart(2, '0');
94
+ return `${baseId}-${d.getUTCFullYear()}-W${ww}`;
95
+ }
96
+
97
+ return baseId;
98
+ }
99
+
26
100
  export class SessionRunner {
27
- constructor(ctx) {
101
+ constructor(ctx, store = null) {
28
102
  this.ctx = ctx;
103
+ this.store = store;
29
104
  }
30
105
 
31
106
  async execute(task, options = {}) {
@@ -207,7 +282,7 @@ export class SessionRunner {
207
282
  const invocation = buildInvocation({ ...task, type: runtime }, cwd);
208
283
  if (invocation.http) return this._runHttp(task, invocation.http, signal);
209
284
 
210
- const { spawn } = await import('node:child_process');
285
+ const { spawn, exec } = await import('node:child_process');
211
286
  return new Promise((resolve, reject) => {
212
287
  let child = null;
213
288
  let settled = false;
@@ -217,14 +292,12 @@ export class SessionRunner {
217
292
  const isPosix = process.platform !== 'win32';
218
293
 
219
294
  const killProcessTree = (sig = 'SIGTERM') => {
220
- if (!child || child.killed) return;
221
- try {
222
- if (isPosix && child.pid) {
223
- try { process.kill(-child.pid, sig); } catch {}
224
- } else if (child.pid) {
225
- child.kill(sig);
295
+ terminateProcessTree(child, sig, {
296
+ isPosix,
297
+ execFn: (cmd) => {
298
+ try { exec(cmd, { windowsHide: true }, () => {}); } catch {}
226
299
  }
227
- } catch {}
300
+ });
228
301
  };
229
302
 
230
303
  const abortHandler = () => {
@@ -349,19 +422,44 @@ export class SessionRunner {
349
422
  }
350
423
 
351
424
  async _runAgentSession(task, runtime, signal, execOptions = {}) {
425
+ let effectiveTask = task;
426
+ if (execOptions?.prevOutput) {
427
+ const prev = String(execOptions.prevOutput);
428
+ const prompt = task.prompt || '';
429
+ const newPrompt = prompt.includes('{{prevOutput}}')
430
+ ? prompt.replaceAll('{{prevOutput}}', prev)
431
+ : `[Previous Output]:\n${prev}\n\n${prompt}`;
432
+ effectiveTask = { ...task, prompt: newPrompt };
433
+ }
434
+ const prep = await this._prepareAgentRun(effectiveTask, runtime, signal);
352
435
  try {
353
- let effectiveTask = task;
354
- if (execOptions?.prevOutput) {
355
- const prev = String(execOptions.prevOutput);
356
- const prompt = task.prompt || '';
357
- const newPrompt = prompt.includes('{{prevOutput}}')
358
- ? prompt.replaceAll('{{prevOutput}}', prev)
359
- : `[Previous Output]:\n${prev}\n\n${prompt}`;
360
- effectiveTask = { ...task, prompt: newPrompt };
361
- }
362
- const prep = await this._prepareAgentRun(effectiveTask, runtime, signal);
363
436
  return await this._executeAgentTurn(task, prep);
364
437
  } catch (err) {
438
+ // Context window overflow auto-recovery in persistent sessions (#145)
439
+ if (prep.targetSessionId && isContextOverflowError(err) && !signal?.aborted) {
440
+ console.warn(`[dsh-cron] Persistent session "${prep.targetSessionId}" exceeded context window (${err.message}). Auto-recovering with fresh session...`);
441
+ try {
442
+ const sessions = this.ctx?.get?.('sessions');
443
+ if (sessions && typeof sessions.archive === 'function') {
444
+ sessions.archive(prep.targetSessionId);
445
+ }
446
+ } catch {}
447
+ const rotatedSessionId = `${prep.targetSessionId}-${Date.now().toString(36)}`;
448
+ console.log(`[dsh-cron] Rotated persistent session "${prep.targetSessionId}" -> "${rotatedSessionId}"`);
449
+ if (this.store) {
450
+ try {
451
+ const stored = this.store.get(task.id);
452
+ if (stored) {
453
+ stored.targetSessionId = rotatedSessionId;
454
+ this.store.set(stored);
455
+ }
456
+ } catch {}
457
+ }
458
+ task.targetSessionId = rotatedSessionId;
459
+ effectiveTask.targetSessionId = rotatedSessionId;
460
+ const freshPrep = await this._prepareAgentRun(effectiveTask, runtime, signal);
461
+ return await this._executeAgentTurn(task, freshPrep);
462
+ }
365
463
  console.error(`[dsh-cron] agent execution failed:`, err.message);
366
464
  throw err;
367
465
  }
@@ -381,7 +479,15 @@ export class SessionRunner {
381
479
  sessionModule = await import('@deepseek-ai/dsh-session');
382
480
  } catch {}
383
481
 
482
+ const targetSessionId = resolveTargetSessionId(task.targetSessionId, task.targetSessionReset);
483
+
384
484
  const mintSessionId = () => {
485
+ if (targetSessionId) {
486
+ if (sessionModule && typeof sessionModule.SessionId === 'function') {
487
+ return sessionModule.SessionId(targetSessionId);
488
+ }
489
+ return targetSessionId;
490
+ }
385
491
  const sid = `cron-exec-${task.id}-${randomUUID()}`;
386
492
  if (sessionModule && typeof sessionModule.SessionId === 'function') {
387
493
  return sessionModule.SessionId(sid);
@@ -393,6 +499,7 @@ export class SessionRunner {
393
499
  return {
394
500
  createUserMessage,
395
501
  mintSessionId,
502
+ targetSessionId,
396
503
  provider: task.provider || defaultSel?.provider,
397
504
  model: task.model || defaultSel?.model || undefined,
398
505
  ...(await this._resolveRunContext(task)),
@@ -456,6 +563,7 @@ ${prompt}`;
456
563
  /** Create the session, drive one turn, and always clean up. */
457
564
  async _executeAgentTurn(task, prep) {
458
565
  let handle = null;
566
+ let isResumed = false;
459
567
  try {
460
568
  // Resolve agent preset (#GH-1): scheduled llm sessions must join an agent preset
461
569
  // (default or task-configured), otherwise tools resolve against the empty global layer.
@@ -473,23 +581,49 @@ ${prompt}`;
473
581
  }
474
582
  }
475
583
 
476
- handle = await this.ctx.agents.create({
477
- sessionId: prep.mintSessionId(),
478
- meta: {
479
- cwd: prep.cwd,
480
- ephemeral: true,
481
- internal: true,
482
- ...(preset ? { agentPreset: preset.id } : {})
483
- },
484
- agentOptions: prep.provider && prep.model ? { provider: prep.provider, model: prep.model } : undefined,
485
- ...(preset ? {
486
- setup: async (agentCtx) => {
487
- if (typeof presets.mount === 'function') {
488
- await presets.mount(agentCtx, preset.id);
489
- }
584
+ const presetSetup = preset ? {
585
+ setup: async (agentCtx) => {
586
+ if (typeof presets.mount === 'function') {
587
+ await presets.mount(agentCtx, preset.id);
490
588
  }
491
- } : {}),
492
- });
589
+ }
590
+ } : {};
591
+
592
+ // If targetSessionId is set, attempt to resume the existing session (#143)
593
+ if (prep.targetSessionId && this.ctx.agents && typeof this.ctx.agents.resume === 'function') {
594
+ try {
595
+ handle = await this.ctx.agents.resume({
596
+ sessionId: prep.targetSessionId,
597
+ meta: {
598
+ cwd: prep.cwd,
599
+ ...(preset ? { agentPreset: preset.id } : {})
600
+ },
601
+ agentOptions: prep.provider && prep.model ? { provider: prep.provider, model: prep.model } : undefined,
602
+ ...presetSetup,
603
+ });
604
+ isResumed = true;
605
+ console.log(`[dsh-cron] resumed existing persistent session: ${prep.targetSessionId}`);
606
+ } catch (resumeErr) {
607
+ console.log(`[dsh-cron] could not resume session ${prep.targetSessionId} (${resumeErr.message}), creating new persistent session`);
608
+ handle = null;
609
+ }
610
+ }
611
+
612
+ // Create new session if not resumed
613
+ if (!handle) {
614
+ handle = await this.ctx.agents.create({
615
+ sessionId: prep.mintSessionId(),
616
+ meta: {
617
+ cwd: prep.cwd,
618
+ ephemeral: !prep.targetSessionId,
619
+ internal: !prep.targetSessionId,
620
+ ...(preset ? { agentPreset: preset.id } : {})
621
+ },
622
+ agentOptions: prep.provider && prep.model ? { provider: prep.provider, model: prep.model } : undefined,
623
+ ...presetSetup,
624
+ });
625
+ }
626
+
493
627
  this._applyPermissionPreset(task, handle);
494
628
  await prep.raceAbort(handle.agent.whenIdle());
495
629
  handle.agent.followup(prep.createUserMessage({
@@ -501,8 +635,11 @@ ${prompt}`;
501
635
  await prep.raceAbort(handle.agent.whenIdle());
502
636
  }
503
637
  const usage = this._extractUsage(handle);
638
+ const sessionNote = isResumed
639
+ ? `resumed: ${prep.targetSessionId}`
640
+ : `session: ${handle.agent.session?.id || 'cron'}`;
504
641
  return {
505
- output: `[dsh-cron] Agent finished turn for task "${task.title}" (session: ${handle.agent.session?.id || 'cron'})`,
642
+ output: `[dsh-cron] Agent finished turn for task "${task.title}" (${sessionNote})`,
506
643
  usage,
507
644
  costUsd: estimateTokenCost(prep.model, usage),
508
645
  sessionId: handle.agent.session?.id || null,
@@ -555,7 +692,8 @@ ${prompt}`;
555
692
  console.warn('[dsh-cron] session dispose notice:', disposeErr.message);
556
693
  }
557
694
  // Best-effort: keep ephemeral cron sessions out of the main chat list (#30).
558
- if (archivedSessionId && this.ctx && this.ctx.get) {
695
+ // Persistent sessions (targetSessionId configured) stay unarchived so users can interact with them (#143).
696
+ if (archivedSessionId && this.ctx && this.ctx.get && !prep.targetSessionId) {
559
697
  try {
560
698
  const sessions = this.ctx.get('sessions');
561
699
  if (sessions && typeof sessions.archive === 'function') {
package/lib/scheduler.js CHANGED
@@ -642,6 +642,11 @@ export class TaskScheduler {
642
642
  const task = this.store.get(taskId);
643
643
  if (!task) return null;
644
644
 
645
+ if (!options.isRetry && task.attempts) {
646
+ task.attempts = 0;
647
+ this.store.set(task);
648
+ }
649
+
645
650
  if (options.dryRun) {
646
651
  const start = Date.now();
647
652
  const outcome = await this.executeWithFallback(task, AbortSignal.timeout(Math.min((task.timeoutSeconds || 1800) * 1000, 30000)), options);
@@ -750,12 +755,15 @@ export class TaskScheduler {
750
755
  if (!silent.skipped) await this.deliverNotifications(task, runInfo);
751
756
  this.finishRun(task, taskId, outcome.status, queuedCount);
752
757
 
753
- if (this.queue.length > 0 && this.running.size < this.maxConcurrent) {
758
+ while (this.queue.length > 0 && this.running.size < this.maxConcurrent) {
754
759
  const nextItem = this.queue.shift();
755
- if (nextItem) {
760
+ if (!nextItem) break;
761
+ const queuedTask = this.store.get(nextItem.taskId);
762
+ if (queuedTask && queuedTask.status === 'active') {
756
763
  setImmediate(() => {
757
764
  this.runTask(nextItem.taskId).catch(() => {});
758
765
  });
766
+ break;
759
767
  }
760
768
  }
761
769
 
@@ -948,12 +956,16 @@ export class TaskScheduler {
948
956
  this.clearRetryTimer(taskId);
949
957
  this.retryTimers.set(taskId, setTimeout(() => {
950
958
  this.retryTimers.delete(taskId);
951
- this.runTask(taskId);
959
+ this.runTask(taskId, { isRetry: true });
952
960
  }, backoffMs));
953
961
  console.log(`[dsh-cron] retry ${attempt}/${maxRetries} for task "${task.title}" in ${backoffMs}ms`);
954
962
  } else if (!failed && task.attempts) {
955
963
  task.attempts = 0;
956
964
  this.store.set(task);
965
+ } else if (failed && maxRetries > 0 && (task.attempts || 0) >= maxRetries) {
966
+ // Retry budget exhausted: reset attempts so the next scheduled run starts fresh (#145)
967
+ task.attempts = 0;
968
+ this.store.set(task);
957
969
  }
958
970
  } catch (err) {
959
971
  console.error('[dsh-cron] retry bookkeeping failed for task', taskId + ':', err.message);
@@ -987,10 +999,31 @@ export class TaskScheduler {
987
999
  } catch {}
988
1000
  this.running.delete(taskId);
989
1001
  }
1002
+ this.queue = this.queue.filter((item) => item.taskId !== taskId);
990
1003
  this.store.set(task);
991
1004
  return task;
992
1005
  }
993
1006
 
1007
+ removeTask(taskId) {
1008
+ this.clearRetryTimer(taskId);
1009
+ if (this.jobs.has(taskId)) {
1010
+ this.jobs.get(taskId).stop();
1011
+ this.jobs.delete(taskId);
1012
+ }
1013
+ if (this.timers.has(taskId)) {
1014
+ clearTimeout(this.timers.get(taskId));
1015
+ this.timers.delete(taskId);
1016
+ }
1017
+ const running = this.running.get(taskId);
1018
+ if (running) {
1019
+ try {
1020
+ running.controller.abort(new Error('Task deleted'));
1021
+ } catch {}
1022
+ this.running.delete(taskId);
1023
+ }
1024
+ this.queue = this.queue.filter((item) => item.taskId !== taskId);
1025
+ }
1026
+
994
1027
  resumeTask(taskId) {
995
1028
  const task = this.store.get(taskId);
996
1029
  if (!task) return null;
package/lib/store.js CHANGED
@@ -38,30 +38,66 @@ export class TaskStore {
38
38
  }
39
39
 
40
40
  init() {
41
- try {
42
- const dir = path.dirname(this.filePath);
43
- if (!fs.existsSync(dir)) {
44
- fs.mkdirSync(dir, { recursive: true });
41
+ const dir = path.dirname(this.filePath);
42
+ if (!fs.existsSync(dir)) {
43
+ fs.mkdirSync(dir, { recursive: true });
44
+ }
45
+ const bakPath = `${this.filePath}.bak`;
46
+
47
+ const loadData = (raw) => {
48
+ const data = JSON.parse(raw);
49
+ this.tasks.clear();
50
+ this.history.clear();
51
+ if (Array.isArray(data.tasks)) {
52
+ for (const t of data.tasks) {
53
+ this.tasks.set(t.id, t);
54
+ }
55
+ }
56
+ if (data.history && typeof data.history === 'object') {
57
+ for (const [k, v] of Object.entries(data.history)) {
58
+ this.history.set(k, Array.isArray(v) ? v : []);
59
+ }
60
+ }
61
+ if (data.settings && typeof data.settings === 'object') {
62
+ this.settings = data.settings;
45
63
  }
46
- if (fs.existsSync(this.filePath)) {
64
+ };
65
+
66
+ if (fs.existsSync(this.filePath)) {
67
+ try {
47
68
  const raw = fs.readFileSync(this.filePath, 'utf-8');
48
- const data = JSON.parse(raw);
49
- if (Array.isArray(data.tasks)) {
50
- for (const t of data.tasks) {
51
- this.tasks.set(t.id, t);
52
- }
69
+ loadData(raw);
70
+ if (!fs.existsSync(bakPath)) {
71
+ try { fs.copyFileSync(this.filePath, bakPath); } catch {}
53
72
  }
54
- if (data.history && typeof data.history === 'object') {
55
- for (const [k, v] of Object.entries(data.history)) {
56
- this.history.set(k, Array.isArray(v) ? v : []);
57
- }
73
+ } catch (err) {
74
+ console.error('[dsh-cron] store read error (corrupted file):', err.message);
75
+ try {
76
+ const corruptedPath = `${this.filePath}.corrupted.${Date.now()}`;
77
+ fs.copyFileSync(this.filePath, corruptedPath);
78
+ console.warn(`[dsh-cron] corrupted store backed up to ${corruptedPath}`);
79
+ } catch (snapErr) {
80
+ console.error('[dsh-cron] failed to snapshot corrupted store:', snapErr.message);
58
81
  }
59
- if (data.settings && typeof data.settings === 'object') {
60
- this.settings = data.settings;
82
+ if (fs.existsSync(bakPath)) {
83
+ try {
84
+ const rawBak = fs.readFileSync(bakPath, 'utf-8');
85
+ loadData(rawBak);
86
+ console.warn(`[dsh-cron] successfully recovered tasks store from ${bakPath}`);
87
+ } catch (bakErr) {
88
+ console.error('[dsh-cron] failed to recover tasks store from .bak:', bakErr.message);
89
+ }
61
90
  }
62
91
  }
63
- } catch (err) {
64
- console.error('[dsh-cron] store read error:', err.message);
92
+ } else if (fs.existsSync(bakPath)) {
93
+ try {
94
+ const rawBak = fs.readFileSync(bakPath, 'utf-8');
95
+ loadData(rawBak);
96
+ try { fs.copyFileSync(bakPath, this.filePath); } catch {}
97
+ console.warn(`[dsh-cron] restored missing tasks store from ${bakPath}`);
98
+ } catch (bakErr) {
99
+ console.error('[dsh-cron] failed to restore tasks store from .bak:', bakErr.message);
100
+ }
65
101
  }
66
102
  }
67
103
 
@@ -83,6 +119,9 @@ export class TaskStore {
83
119
  const tmp = `${this.filePath}.tmp.${process.pid}_${Date.now()}_${rand}`;
84
120
  fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
85
121
  fs.renameSync(tmp, this.filePath);
122
+ try {
123
+ fs.copyFileSync(this.filePath, `${this.filePath}.bak`);
124
+ } catch {}
86
125
  } catch (err) {
87
126
  console.error('[dsh-cron] store save error:', err.message);
88
127
  }
@@ -250,6 +289,8 @@ export class TaskStore {
250
289
  selfHealingCommand: task.selfHealingCommand !== undefined ? String(task.selfHealingCommand).trim() : (prev.selfHealingCommand || ''),
251
290
  autoDiagnose: task.autoDiagnose !== undefined ? Boolean(task.autoDiagnose) : (prev.autoDiagnose || false),
252
291
  agentPreset: task.agentPreset !== undefined ? String(task.agentPreset).trim() : (prev.agentPreset || ''),
292
+ targetSessionId: task.targetSessionId !== undefined ? String(task.targetSessionId).trim() : (prev.targetSessionId || ''),
293
+ targetSessionReset: task.targetSessionReset !== undefined ? String(task.targetSessionReset).trim() : (prev.targetSessionReset || 'never'),
253
294
  totalTokens: prev.totalTokens || 0,
254
295
  totalCostUsd: prev.totalCostUsd || 0,
255
296
  createdAt: prev.createdAt || task.createdAt || now,
@@ -358,6 +399,9 @@ export class TaskStore {
358
399
  }
359
400
  if (!Array.isArray(archive[id])) archive[id] = [];
360
401
  archive[id].push(...overflowRuns);
402
+ if (archive[id].length > 1000) {
403
+ archive[id] = archive[id].slice(-1000);
404
+ }
361
405
  // Write atomically with tmp file
362
406
  const rand = Math.random().toString(36).slice(2, 8);
363
407
  const tmp = `${archivePath}.tmp.${process.pid}_${Date.now()}_${rand}`;
@@ -31,6 +31,8 @@ export const PATCHABLE_TASK_FIELDS = [
31
31
  'retryBackoffMs',
32
32
  'permissionPreset',
33
33
  'agentPreset',
34
+ 'targetSessionId',
35
+ 'targetSessionReset',
34
36
  'env',
35
37
  'cwd',
36
38
  'workspaceId',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-cron",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "Scheduled cron tasks, background automation and agent execution for DeepSeek Harness.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",