@goodandready/dsh-cron 0.2.13 → 0.2.15
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 +20 -0
- package/README.ru.md +20 -0
- package/README.zh.md +20 -0
- package/lib/api.js +5 -1
- package/lib/client.js +10 -5
- package/lib/index.js +1 -1
- package/lib/runner.js +81 -19
- package/lib/scheduler.js +36 -3
- package/lib/store.js +60 -18
- package/package.json +5 -4
- package/docs/README.ru.md +0 -501
- package/docs/README.zh.md +0 -501
package/README.md
CHANGED
|
@@ -372,6 +372,26 @@ Developer-facing, no behaviour change. `parseScheduleExpression` was split into
|
|
|
372
372
|
|
|
373
373
|
---
|
|
374
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
|
+
### 27. Modal Dialog Viewport Bounds & Package Hygiene (v0.2.15, #153, #148, #151, #152)
|
|
386
|
+
- **Viewport Height Bounding & Sticky Footer**: Modal forms (including task creation, editing, and ready recipe presets) now strictly respect viewport boundaries with `max-height: min(90vh, calc(100vh - 36px))` and smooth internal scrolling. The modal action footer (`Cancel`, `Save`, `Create`) is pinned via sticky positioning (`position: sticky`), guaranteeing that critical buttons remain immediately accessible and never clipped regardless of form complexity or display scale.
|
|
387
|
+
- **Overlay Scroll Protection**: Modal overlays now provide safe viewport padding and overflow handling (`overflow-y: auto`), preventing flexbox centering clipping on small screens.
|
|
388
|
+
- **Package Weight Optimization**: Removed redundant documentation duplicates from npm distribution files, trimming tarball weight by over 32 kB and unpacked size by ~102 kB.
|
|
389
|
+
- **Cordis Client Inject Compliance**: Explicitly declared `locale` and `slots` dependencies in `package.json` client injection manifest (`dsh.client.inject`).
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
|
|
375
395
|
## 📦 Installation
|
|
376
396
|
|
|
377
397
|
Install into your DeepSeek Harness web profile:
|
package/README.ru.md
CHANGED
|
@@ -248,6 +248,26 @@ cron_create_task({
|
|
|
248
248
|
|
|
249
249
|
---
|
|
250
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
|
+
### 27. Ограничение высоты модальных окон и гигиена пакета (v0.2.15, #153, #148, #151, #152)
|
|
262
|
+
- **Ограничение по высоте экрана и липкий подвал**: Модальные окна (включая создание, редактирование и готовые шаблоны-рецепты) теперь строго ограничены высотой экрана `max-height: min(90vh, calc(100vh - 36px))` с плавным внутренним скроллом. Подвал окна с кнопками («Отмена», «Сохранить», «Создать») закреплён через `position: sticky`, гарантируя постоянную доступность кнопок действий при любой длине формы и любом разрешении экрана.
|
|
263
|
+
- **Защита оверлея от вылетов**: Оверлей модального окна получил безопасные отступы и свойство `overflow-y: auto`, исключая срезание контента при flexbox-центрировании на компактных дисплеях.
|
|
264
|
+
- **Оптимизация размера npm-пакета**: Удалены дублирующие файлы документации из списка дистрибуции npm, снизив вес архива более чем на 32 kB, а распакованный размер — на 102 kB.
|
|
265
|
+
- **Соответствие манифеста клиента Cordis**: В `package.json` явно задекларированы зависимости инжекции `locale` и `slots` в секции `dsh.client.inject`.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
|
|
251
271
|
## 📦 Установка
|
|
252
272
|
|
|
253
273
|
```bash
|
package/README.zh.md
CHANGED
|
@@ -371,6 +371,26 @@ bash deploy.sh verify [exact-version]
|
|
|
371
371
|
|
|
372
372
|
---
|
|
373
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
|
+
### 27. 弹窗视口高度自适应与包体积精简 (v0.2.15, #153, #148, #151, #152)
|
|
385
|
+
- **视口高度约束与粘性底部操作栏**:所有弹窗(包括任务编辑、新建及推荐模板预设)现已严格受控于视口尺寸 `max-height: min(90vh, calc(100vh - 36px))`,并内置平滑纵向滚动条。弹窗操作栏(“取消”、“保存”、“创建”)采用 `position: sticky` 底部悬浮固定,确保无论表单项多长或屏幕分辨率高低,操作按钮始终清晰可见且可随时点击。
|
|
386
|
+
- **遮罩层滚动溢出保护**:弹窗遮罩层增加了安全边距与 `overflow-y: auto`,防止小屏设备在 Flex 居中时发生头部或底部截断。
|
|
387
|
+
- **npm 包体积深度精简**:从 npm 分发清单中剔除了多余的重复文档副本,使 tarball 体积立减 32 kB 以上,解压后体积减少约 102 kB。
|
|
388
|
+
- **Cordis 客户端注入依赖规范化**:在 `package.json` 的 `dsh.client.inject` 中完整声明了 `locale` 和 `slots` 服务依赖。
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
|
|
374
394
|
## 📦 安装
|
|
375
395
|
|
|
376
396
|
```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.
|
|
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
|
@@ -883,9 +883,13 @@ window.__ModuleLoader__.load({
|
|
|
883
883
|
.dsh-cron-rec-time { font-size: 13px; color: var(--dsw-alias-label-secondary, #9ca3af); font-weight: normal; margin-left: 8px; }
|
|
884
884
|
.dsh-cron-rec-desc { font-size: 13px; color: var(--dsw-alias-label-tertiary, #71717a); }
|
|
885
885
|
|
|
886
|
-
.dsh-cron-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.
|
|
887
|
-
.dsh-cron-modal { background: var(--dsw-alias-bg-layer-2, #1f1f1f); border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 16px; width: 100%; max-width: 540px; padding: 24px; box-shadow: 0 20px 40px rgba(0,0,0,0.6); box-sizing: border-box; }
|
|
888
|
-
.dsh-cron-modal-scroll { max-width:
|
|
886
|
+
.dsh-cron-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.75); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 16px; box-sizing: border-box; overflow-y: auto; }
|
|
887
|
+
.dsh-cron-modal { background: var(--dsw-alias-bg-layer-2, #1f1f1f); border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 16px; width: 100%; max-width: 540px; max-height: min(90vh, calc(100vh - 36px)); overflow-y: auto; padding: 24px; box-shadow: 0 20px 40px rgba(0,0,0,0.6); box-sizing: border-box; }
|
|
888
|
+
.dsh-cron-modal-scroll { max-width: 640px; max-height: min(90vh, calc(100vh - 36px)); overflow-y: auto; }
|
|
889
|
+
.dsh-cron-modal::-webkit-scrollbar { width: 6px; }
|
|
890
|
+
.dsh-cron-modal::-webkit-scrollbar-track { background: transparent; }
|
|
891
|
+
.dsh-cron-modal::-webkit-scrollbar-thumb { background: var(--dsw-alias-border-l2, #383838); border-radius: 3px; }
|
|
892
|
+
.dsh-cron-modal::-webkit-scrollbar-thumb:hover { background: var(--dsw-alias-label-tertiary, #555); }
|
|
889
893
|
.dsh-cron-section-head { display: flex; align-items: center; justify-content: space-between; width: 100%; background: none; border: none; padding: 0; margin-bottom: 10px; font: inherit; font-weight: 500; font-size: 12.5px; color: var(--dsw-alias-label-secondary, #aaa); cursor: pointer; }
|
|
890
894
|
.dsh-cron-section-head:hover { color: var(--dsw-alias-label-primary, #eee); }
|
|
891
895
|
.dsh-cron-section-chevron { transition: transform 0.15s ease; display: inline-block; }
|
|
@@ -899,7 +903,7 @@ window.__ModuleLoader__.load({
|
|
|
899
903
|
.dsh-cron-form-group select { cursor: pointer; }
|
|
900
904
|
.dsh-cron-form-group textarea { min-height: 80px; resize: vertical; }
|
|
901
905
|
.dsh-cron-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
902
|
-
.dsh-cron-modal-foot { display: flex; justify-content: flex-end; align-items: center; gap: 10px; margin-top: 20px; }
|
|
906
|
+
.dsh-cron-modal-foot { display: flex; justify-content: flex-end; align-items: center; gap: 10px; margin-top: 20px; position: sticky; bottom: -24px; background: var(--dsw-alias-bg-layer-2, #1f1f1f); border-top: 1px solid var(--dsw-alias-border-l2, #2e2e2e); padding-top: 14px; padding-bottom: 4px; margin-left: -24px; margin-right: -24px; padding-left: 24px; padding-right: 24px; z-index: 10; }
|
|
903
907
|
.dsh-cron-btn-primary { background: var(--dsw-alias-label-primary, #ededed); color: var(--dsw-alias-bg-layer-3, #111); border: 1px solid transparent; border-radius: 8px; padding: 7px 14px; font-size: 13px; font-weight: 500; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; }
|
|
904
908
|
.dsh-cron-btn-primary:hover:not(:disabled) { opacity: 0.88; }
|
|
905
909
|
.dsh-cron-btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
@@ -984,6 +988,7 @@ window.__ModuleLoader__.load({
|
|
|
984
988
|
.dsh-cron-channel-grid { grid-template-columns: 1fr; }
|
|
985
989
|
.dsh-cron-filter-panel { grid-template-columns: 1fr; }
|
|
986
990
|
.dsh-cron-modal { padding: 16px; border-radius: 12px; }
|
|
991
|
+
.dsh-cron-modal-foot { bottom: -16px; margin-left: -16px; margin-right: -16px; padding-left: 16px; padding-right: 16px; }
|
|
987
992
|
.dsh-cron-modal-scroll { max-height: 92vh; }
|
|
988
993
|
.dsh-cron-task-item { flex-wrap: wrap; gap: 10px; }
|
|
989
994
|
.dsh-cron-task-actions { margin-left: auto; }
|
|
@@ -2320,7 +2325,7 @@ window.__ModuleLoader__.load({
|
|
|
2320
2325
|
// 1. Manual creation / Edit modal
|
|
2321
2326
|
manualModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setManualModalOpen(false) },
|
|
2322
2327
|
React.createElement('div', {
|
|
2323
|
-
className: 'dsh-cron-modal',
|
|
2328
|
+
className: 'dsh-cron-modal dsh-cron-modal-scroll',
|
|
2324
2329
|
style: { maxWidth: '640px' },
|
|
2325
2330
|
role: 'dialog',
|
|
2326
2331
|
'aria-modal': 'true',
|
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
|
*/
|
|
@@ -60,8 +98,9 @@ export function resolveTargetSessionId(targetSessionId, resetPolicy = 'never', d
|
|
|
60
98
|
}
|
|
61
99
|
|
|
62
100
|
export class SessionRunner {
|
|
63
|
-
constructor(ctx) {
|
|
101
|
+
constructor(ctx, store = null) {
|
|
64
102
|
this.ctx = ctx;
|
|
103
|
+
this.store = store;
|
|
65
104
|
}
|
|
66
105
|
|
|
67
106
|
async execute(task, options = {}) {
|
|
@@ -243,7 +282,7 @@ export class SessionRunner {
|
|
|
243
282
|
const invocation = buildInvocation({ ...task, type: runtime }, cwd);
|
|
244
283
|
if (invocation.http) return this._runHttp(task, invocation.http, signal);
|
|
245
284
|
|
|
246
|
-
const { spawn } = await import('node:child_process');
|
|
285
|
+
const { spawn, exec } = await import('node:child_process');
|
|
247
286
|
return new Promise((resolve, reject) => {
|
|
248
287
|
let child = null;
|
|
249
288
|
let settled = false;
|
|
@@ -253,14 +292,12 @@ export class SessionRunner {
|
|
|
253
292
|
const isPosix = process.platform !== 'win32';
|
|
254
293
|
|
|
255
294
|
const killProcessTree = (sig = 'SIGTERM') => {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
try {
|
|
260
|
-
} else if (child.pid) {
|
|
261
|
-
child.kill(sig);
|
|
295
|
+
terminateProcessTree(child, sig, {
|
|
296
|
+
isPosix,
|
|
297
|
+
execFn: (cmd) => {
|
|
298
|
+
try { exec(cmd, { windowsHide: true }, () => {}); } catch {}
|
|
262
299
|
}
|
|
263
|
-
}
|
|
300
|
+
});
|
|
264
301
|
};
|
|
265
302
|
|
|
266
303
|
const abortHandler = () => {
|
|
@@ -385,19 +422,44 @@ export class SessionRunner {
|
|
|
385
422
|
}
|
|
386
423
|
|
|
387
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);
|
|
388
435
|
try {
|
|
389
|
-
let effectiveTask = task;
|
|
390
|
-
if (execOptions?.prevOutput) {
|
|
391
|
-
const prev = String(execOptions.prevOutput);
|
|
392
|
-
const prompt = task.prompt || '';
|
|
393
|
-
const newPrompt = prompt.includes('{{prevOutput}}')
|
|
394
|
-
? prompt.replaceAll('{{prevOutput}}', prev)
|
|
395
|
-
: `[Previous Output]:\n${prev}\n\n${prompt}`;
|
|
396
|
-
effectiveTask = { ...task, prompt: newPrompt };
|
|
397
|
-
}
|
|
398
|
-
const prep = await this._prepareAgentRun(effectiveTask, runtime, signal);
|
|
399
436
|
return await this._executeAgentTurn(task, prep);
|
|
400
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
|
+
}
|
|
401
463
|
console.error(`[dsh-cron] agent execution failed:`, err.message);
|
|
402
464
|
throw err;
|
|
403
465
|
}
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (fs.existsSync(this.filePath)) {
|
|
67
|
+
try {
|
|
47
68
|
const raw = fs.readFileSync(this.filePath, 'utf-8');
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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 (
|
|
60
|
-
|
|
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
|
-
}
|
|
64
|
-
|
|
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
|
}
|
|
@@ -360,6 +399,9 @@ export class TaskStore {
|
|
|
360
399
|
}
|
|
361
400
|
if (!Array.isArray(archive[id])) archive[id] = [];
|
|
362
401
|
archive[id].push(...overflowRuns);
|
|
402
|
+
if (archive[id].length > 1000) {
|
|
403
|
+
archive[id] = archive[id].slice(-1000);
|
|
404
|
+
}
|
|
363
405
|
// Write atomically with tmp file
|
|
364
406
|
const rand = Math.random().toString(36).slice(2, 8);
|
|
365
407
|
const tmp = `${archivePath}.tmp.${process.pid}_${Date.now()}_${rand}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-cron",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15",
|
|
4
4
|
"description": "Scheduled cron tasks, background automation and agent execution for DeepSeek Harness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -14,8 +14,6 @@
|
|
|
14
14
|
"lib/",
|
|
15
15
|
"cordis.patch.yml",
|
|
16
16
|
"README.md",
|
|
17
|
-
"docs/README.ru.md",
|
|
18
|
-
"docs/README.zh.md",
|
|
19
17
|
"LICENSE"
|
|
20
18
|
],
|
|
21
19
|
"scripts": {
|
|
@@ -44,7 +42,10 @@
|
|
|
44
42
|
},
|
|
45
43
|
"client": {
|
|
46
44
|
"platform": "web",
|
|
47
|
-
"inject": [
|
|
45
|
+
"inject": [
|
|
46
|
+
"locale",
|
|
47
|
+
"slots"
|
|
48
|
+
]
|
|
48
49
|
}
|
|
49
50
|
},
|
|
50
51
|
"dependencies": {
|