@goodandready/dsh-cron 0.2.10 → 0.2.12
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 +27 -2
- package/README.ru.md +488 -0
- package/README.zh.md +488 -0
- package/docs/README.ru.md +41 -1
- package/docs/README.zh.md +41 -1
- package/lib/api.js +93 -2
- package/lib/chat-start.js +21 -1
- package/lib/client.js +595 -3
- package/lib/index.js +18 -0
- package/lib/runner.js +29 -1
- package/lib/scheduler.js +239 -4
- package/lib/store.js +34 -0
- package/lib/task-transfer.js +9 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -393,6 +393,24 @@ export function apply(ctx, config) {
|
|
|
393
393
|
handler: createCronApiHandler(store, scheduler, null)
|
|
394
394
|
}), 'dsh-cron: /action/:id/:action');
|
|
395
395
|
|
|
396
|
+
ctx.effect(() => ctx.webServer.register({
|
|
397
|
+
kind: 'prefix',
|
|
398
|
+
path: '/dsh-cron/telegram',
|
|
399
|
+
handler: createCronApiHandler(store, scheduler, null)
|
|
400
|
+
}), 'dsh-cron: /telegram[...]');
|
|
401
|
+
|
|
402
|
+
ctx.effect(() => ctx.webServer.register({
|
|
403
|
+
kind: 'prefix',
|
|
404
|
+
path: '/dsh-cron/heartbeat-ping',
|
|
405
|
+
handler: createCronApiHandler(store, scheduler, null)
|
|
406
|
+
}), 'dsh-cron: /heartbeat-ping[...]');
|
|
407
|
+
|
|
408
|
+
ctx.effect(() => ctx.webServer.register({
|
|
409
|
+
kind: 'prefix',
|
|
410
|
+
path: '/dsh-cron/schedule',
|
|
411
|
+
handler: createCronApiHandler(store, scheduler, null)
|
|
412
|
+
}), 'dsh-cron: /schedule[...]');
|
|
413
|
+
|
|
396
414
|
// GET /dsh-cron/metrics (#53) — Prometheus text exposition
|
|
397
415
|
ctx.effect(() => ctx.webServer.register({
|
|
398
416
|
kind: 'exact',
|
package/lib/runner.js
CHANGED
|
@@ -457,10 +457,38 @@ ${prompt}`;
|
|
|
457
457
|
async _executeAgentTurn(task, prep) {
|
|
458
458
|
let handle = null;
|
|
459
459
|
try {
|
|
460
|
+
// Resolve agent preset (#GH-1): scheduled llm sessions must join an agent preset
|
|
461
|
+
// (default or task-configured), otherwise tools resolve against the empty global layer.
|
|
462
|
+
const presets = this.ctx.agentPresets || (this.ctx.get && this.ctx.get('agentPresets'));
|
|
463
|
+
let preset = null;
|
|
464
|
+
if (presets && typeof presets.resolve === 'function') {
|
|
465
|
+
try {
|
|
466
|
+
preset = await presets.resolve(task.agentPreset || undefined);
|
|
467
|
+
if (preset && typeof presets.standingKeyFor === 'function') {
|
|
468
|
+
await presets.standingKeyFor(preset.id);
|
|
469
|
+
}
|
|
470
|
+
} catch (presetErr) {
|
|
471
|
+
console.warn('[dsh-cron] agent preset unavailable, running without one:', presetErr && presetErr.message);
|
|
472
|
+
preset = null;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
460
476
|
handle = await this.ctx.agents.create({
|
|
461
477
|
sessionId: prep.mintSessionId(),
|
|
462
|
-
meta: {
|
|
478
|
+
meta: {
|
|
479
|
+
cwd: prep.cwd,
|
|
480
|
+
ephemeral: true,
|
|
481
|
+
internal: true,
|
|
482
|
+
...(preset ? { agentPreset: preset.id } : {})
|
|
483
|
+
},
|
|
463
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
|
+
}
|
|
490
|
+
}
|
|
491
|
+
} : {}),
|
|
464
492
|
});
|
|
465
493
|
this._applyPermissionPreset(task, handle);
|
|
466
494
|
await prep.raceAbort(handle.agent.whenIdle());
|
package/lib/scheduler.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Cron } from 'croner';
|
|
2
|
+
import { exec } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
2
4
|
import { parseLlmActionDirectives, executeLlmActionDirectives } from './llm-actions.js';
|
|
3
5
|
import { applySilentRule } from './silent-rule.js';
|
|
4
6
|
import { inspectFailure } from './failure-inspector.js';
|
|
@@ -166,6 +168,98 @@ export function parseScheduleExpression(input) {
|
|
|
166
168
|
|| parseCronExpression(str);
|
|
167
169
|
}
|
|
168
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Preview future execution timestamps for a cron or interval schedule.
|
|
173
|
+
*/
|
|
174
|
+
export function previewSchedule(scheduleStr, options = {}) {
|
|
175
|
+
const str = String(scheduleStr || '').trim();
|
|
176
|
+
if (!str) throw new Error('Schedule expression must not be empty');
|
|
177
|
+
const parsed = parseScheduleExpression(str);
|
|
178
|
+
const count = Math.min(Math.max(Number(options.count) || 5, 1), 10);
|
|
179
|
+
const timezone = options.timezone || undefined;
|
|
180
|
+
|
|
181
|
+
if (parsed.isOneShot) {
|
|
182
|
+
return {
|
|
183
|
+
isOneShot: true,
|
|
184
|
+
humanText: parsed.humanText,
|
|
185
|
+
runs: [new Date(parsed.nextRun).toISOString()],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const job = new Cron(parsed.cronPattern, {
|
|
190
|
+
timezone,
|
|
191
|
+
sloppyRanges: true,
|
|
192
|
+
});
|
|
193
|
+
const runs = [];
|
|
194
|
+
let current = new Date();
|
|
195
|
+
for (let i = 0; i < count; i++) {
|
|
196
|
+
const next = job.nextRun(current);
|
|
197
|
+
if (!next) break;
|
|
198
|
+
runs.push(next.toISOString());
|
|
199
|
+
current = new Date(next.getTime() + 1000);
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
isOneShot: false,
|
|
203
|
+
cronPattern: parsed.cronPattern,
|
|
204
|
+
humanText: describeCron(parsed.cronPattern),
|
|
205
|
+
runs,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Execute pre-flight check gate before running task body.
|
|
211
|
+
*/
|
|
212
|
+
export async function executePreflight(task) {
|
|
213
|
+
const type = String(task.preflightType || 'none').toLowerCase();
|
|
214
|
+
const target = String(task.preflightTarget || '').trim();
|
|
215
|
+
if (!type || type === 'none' || !target) {
|
|
216
|
+
return { ok: true };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (type === 'http') {
|
|
220
|
+
try {
|
|
221
|
+
const controller = new AbortController();
|
|
222
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
223
|
+
const res = await fetch(target, { signal: controller.signal });
|
|
224
|
+
clearTimeout(timeout);
|
|
225
|
+
if (res.ok) return { ok: true };
|
|
226
|
+
return { ok: false, reason: `HTTP check returned status ${res.status}` };
|
|
227
|
+
} catch (err) {
|
|
228
|
+
return { ok: false, reason: `HTTP check failed: ${err.message}` };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (type === 'command') {
|
|
233
|
+
return new Promise((resolve) => {
|
|
234
|
+
exec(target, { timeout: 5000 }, (err, stdout, stderr) => {
|
|
235
|
+
if (err) {
|
|
236
|
+
resolve({ ok: false, reason: `Preflight command failed (code ${err.code || 1}): ${(stderr || stdout || err.message).trim().slice(0, 150)}` });
|
|
237
|
+
} else {
|
|
238
|
+
resolve({ ok: true });
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (type === 'disk') {
|
|
245
|
+
const requiredMb = Number(target) || 100;
|
|
246
|
+
try {
|
|
247
|
+
if (typeof fs.statfsSync === 'function') {
|
|
248
|
+
const stats = fs.statfsSync(process.cwd());
|
|
249
|
+
const freeMb = Math.round((stats.bavail * stats.bsize) / (1024 * 1024));
|
|
250
|
+
if (freeMb < requiredMb) {
|
|
251
|
+
return { ok: false, reason: `Free disk space (${freeMb} MB) is below required ${requiredMb} MB` };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return { ok: true };
|
|
255
|
+
} catch {
|
|
256
|
+
return { ok: true }; // Fail-open
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return { ok: true };
|
|
261
|
+
}
|
|
262
|
+
|
|
169
263
|
export class TaskScheduler {
|
|
170
264
|
constructor(store, executeFn, options = {}) {
|
|
171
265
|
this.store = store;
|
|
@@ -179,6 +273,8 @@ export class TaskScheduler {
|
|
|
179
273
|
this.retryTimers = new Map(); // taskId -> setTimeout handle (for #18 retries)
|
|
180
274
|
this.running = new Map(); // taskId -> { controller, startedAt, queueCount }
|
|
181
275
|
this.runCounters = {}; // status -> finished runs since start (#53, /dsh-cron/metrics)
|
|
276
|
+
this.queue = []; // [{ taskId, priority, queuedAt, options }]
|
|
277
|
+
this.heartbeatTimer = null;
|
|
182
278
|
}
|
|
183
279
|
|
|
184
280
|
isRunning(taskId) {
|
|
@@ -202,6 +298,67 @@ export class TaskScheduler {
|
|
|
202
298
|
}
|
|
203
299
|
}
|
|
204
300
|
|
|
301
|
+
startHeartbeatWatcher(intervalMs = 30000) {
|
|
302
|
+
this.stopHeartbeatWatcher();
|
|
303
|
+
this.heartbeatTimer = setInterval(() => this.checkHeartbeats(), intervalMs);
|
|
304
|
+
if (this.heartbeatTimer && typeof this.heartbeatTimer.unref === 'function') {
|
|
305
|
+
this.heartbeatTimer.unref();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
stopHeartbeatWatcher() {
|
|
310
|
+
if (this.heartbeatTimer) {
|
|
311
|
+
clearInterval(this.heartbeatTimer);
|
|
312
|
+
this.heartbeatTimer = null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async checkHeartbeats() {
|
|
317
|
+
const now = Date.now();
|
|
318
|
+
const tasks = typeof this.store.list === 'function' ? this.store.list({ status: 'active' }) : Array.from(this.store.tasks.values());
|
|
319
|
+
for (const task of tasks) {
|
|
320
|
+
if (task.status !== 'active' || !task.heartbeatIntervalSeconds || task.heartbeatIntervalSeconds <= 0) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const grace = task.gracePeriodSeconds !== undefined ? task.gracePeriodSeconds : 300;
|
|
324
|
+
const last = task.lastPingAt || task.createdAt || now;
|
|
325
|
+
const deadline = last + (task.heartbeatIntervalSeconds + grace) * 1000;
|
|
326
|
+
|
|
327
|
+
if (now > deadline && !task.heartbeatAlerted) {
|
|
328
|
+
task.heartbeatAlerted = true;
|
|
329
|
+
task.lastStatus = 'missed';
|
|
330
|
+
task.updatedAt = now;
|
|
331
|
+
this.store.save();
|
|
332
|
+
|
|
333
|
+
console.warn(`[dsh-cron] Heartbeat missed for "${task.title}" (${task.id})!`);
|
|
334
|
+
const runInfo = {
|
|
335
|
+
at: now,
|
|
336
|
+
status: 'missed',
|
|
337
|
+
durationMs: 0,
|
|
338
|
+
output: `Heartbeat missed: expected ping within ${task.heartbeatIntervalSeconds + grace}s, last ping was at ${new Date(last).toISOString()}`,
|
|
339
|
+
error: 'Heartbeat deadline exceeded',
|
|
340
|
+
usage: { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0 },
|
|
341
|
+
costUsd: 0,
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
this.store.recordRun(task.id, runInfo);
|
|
346
|
+
} catch {}
|
|
347
|
+
|
|
348
|
+
await this.deliverNotifications(task, runInfo);
|
|
349
|
+
|
|
350
|
+
if (task.onFailure && String(task.onFailure).trim()) {
|
|
351
|
+
const targetId = task.onFailure.trim();
|
|
352
|
+
this.runNow(targetId, {
|
|
353
|
+
chainDepth: 1,
|
|
354
|
+
prevOutput: runInfo.output,
|
|
355
|
+
prevTaskId: task.id,
|
|
356
|
+
}).catch((err) => console.warn(`[dsh-cron] heartbeat onFailure trigger error:`, err.message));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
205
362
|
start() {
|
|
206
363
|
const tasks = this.store.list();
|
|
207
364
|
const now = Date.now();
|
|
@@ -241,9 +398,11 @@ export class TaskScheduler {
|
|
|
241
398
|
}
|
|
242
399
|
this.scheduleTask(task);
|
|
243
400
|
}
|
|
401
|
+
this.startHeartbeatWatcher();
|
|
244
402
|
}
|
|
245
403
|
|
|
246
404
|
stopAll() {
|
|
405
|
+
this.stopHeartbeatWatcher();
|
|
247
406
|
for (const [id, job] of this.jobs.entries()) {
|
|
248
407
|
try {
|
|
249
408
|
job.stop();
|
|
@@ -348,6 +507,12 @@ export class TaskScheduler {
|
|
|
348
507
|
// skipped and recorded; already-running tasks are never throttled against
|
|
349
508
|
// themselves.
|
|
350
509
|
if (!this.running.has(taskId) && this.maxConcurrent > 0 && this.running.size >= this.maxConcurrent) {
|
|
510
|
+
if (task.overlapPolicy === 'queue') {
|
|
511
|
+
console.log(`[dsh-cron] Task "${task.title}" (${taskId}) queued by concurrency limit (${this.maxConcurrent})`);
|
|
512
|
+
this.queue.push({ taskId, priority: task.priority !== undefined ? task.priority : 5, queuedAt: Date.now() });
|
|
513
|
+
this.queue.sort((a, b) => (a.priority - b.priority) || (a.queuedAt - b.queuedAt));
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
351
516
|
console.log(`[dsh-cron] Task "${task.title}" (${taskId}) skipped: concurrency limit (${this.maxConcurrent})`);
|
|
352
517
|
this.recordSkipped(taskId, `Skipped: concurrency limit reached (${this.maxConcurrent} parallel runs)`);
|
|
353
518
|
return null;
|
|
@@ -475,14 +640,39 @@ export class TaskScheduler {
|
|
|
475
640
|
|
|
476
641
|
async runTask(taskId, options = {}) {
|
|
477
642
|
const task = this.store.get(taskId);
|
|
478
|
-
if (!task) return;
|
|
643
|
+
if (!task) return null;
|
|
644
|
+
|
|
645
|
+
if (options.dryRun) {
|
|
646
|
+
const start = Date.now();
|
|
647
|
+
const outcome = await this.executeWithFallback(task, AbortSignal.timeout(Math.min((task.timeoutSeconds || 1800) * 1000, 30000)), options);
|
|
648
|
+
return {
|
|
649
|
+
ok: true,
|
|
650
|
+
dryRun: true,
|
|
651
|
+
status: outcome.status,
|
|
652
|
+
output: outcome.output,
|
|
653
|
+
error: outcome.error,
|
|
654
|
+
durationMs: Date.now() - start,
|
|
655
|
+
usage: outcome.usage,
|
|
656
|
+
costUsd: outcome.costUsd,
|
|
657
|
+
model: outcome.model || task.model || '',
|
|
658
|
+
};
|
|
659
|
+
}
|
|
479
660
|
|
|
480
661
|
const currentRun = this.beginRun(task, taskId);
|
|
481
|
-
if (currentRun === null) return;
|
|
662
|
+
if (currentRun === null) return null;
|
|
663
|
+
|
|
664
|
+
const preflight = await executePreflight(task);
|
|
665
|
+
if (!preflight.ok) {
|
|
666
|
+
console.log(`[dsh-cron] task "${task.title}" (${taskId}) skipped: ${preflight.reason}`);
|
|
667
|
+
this.running.delete(taskId);
|
|
668
|
+
this.recordSkipped(taskId, `Preflight check skipped: ${preflight.reason}`);
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
482
671
|
|
|
483
672
|
const start = Date.now();
|
|
484
673
|
const outcome = await this.executeWithFallback(task, currentRun.controller.signal, options);
|
|
485
674
|
await this.completeRun(task, taskId, currentRun, outcome, start, options);
|
|
675
|
+
return outcome;
|
|
486
676
|
}
|
|
487
677
|
|
|
488
678
|
/**
|
|
@@ -490,6 +680,8 @@ export class TaskScheduler {
|
|
|
490
680
|
* bookkeeping. Kept separate from runTask so the run flow reads in steps.
|
|
491
681
|
*/
|
|
492
682
|
async completeRun(task, taskId, currentRun, outcome, start, options = {}) {
|
|
683
|
+
const isSuccess = outcome.status === 'success';
|
|
684
|
+
const isFailure = outcome.status === 'error' || outcome.status === 'timeout';
|
|
493
685
|
const runInfo = {
|
|
494
686
|
at: start,
|
|
495
687
|
status: outcome.status,
|
|
@@ -507,6 +699,40 @@ export class TaskScheduler {
|
|
|
507
699
|
|
|
508
700
|
// #43/#44 decide before the run is recorded, so the single history entry
|
|
509
701
|
// already carries the diagnosis and the silence verdict.
|
|
702
|
+
// 3. Self-healing & Auto-diagnose (#139)
|
|
703
|
+
if (isFailure && task.selfHealingCommand) {
|
|
704
|
+
console.log(`[dsh-cron] running self-healing command for task "${task.title}": ${task.selfHealingCommand}`);
|
|
705
|
+
try {
|
|
706
|
+
await new Promise((resolve) => {
|
|
707
|
+
exec(task.selfHealingCommand, { timeout: 30000 }, (shErr, stdout, stderr) => {
|
|
708
|
+
runInfo.selfHealing = {
|
|
709
|
+
command: task.selfHealingCommand,
|
|
710
|
+
success: !shErr,
|
|
711
|
+
output: (stdout + (stderr ? '\n' + stderr : '')).trim().slice(0, 500),
|
|
712
|
+
};
|
|
713
|
+
resolve();
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
} catch (shErr) {
|
|
717
|
+
runInfo.selfHealing = { command: task.selfHealingCommand, success: false, error: shErr.message };
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (isFailure && task.autoDiagnose && typeof this.executeFn === 'function') {
|
|
722
|
+
try {
|
|
723
|
+
const diagRes = await this.executeFn({
|
|
724
|
+
type: 'llm',
|
|
725
|
+
prompt: `Diagnose this task failure in 1-2 concise sentences. Error: ${outcome.error || 'unknown'}. Recent output: ${(outcome.output || '').slice(-300)}`,
|
|
726
|
+
model: task.fallbackModel || task.model,
|
|
727
|
+
}, { signal: AbortSignal.timeout(10000) });
|
|
728
|
+
if (diagRes && diagRes.output) {
|
|
729
|
+
runInfo.autoDiagnosis = diagRes.output.trim();
|
|
730
|
+
}
|
|
731
|
+
} catch {
|
|
732
|
+
// Fail-open
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
510
736
|
const silent = await this.applyModelAssists(task, runInfo);
|
|
511
737
|
|
|
512
738
|
try {
|
|
@@ -524,6 +750,15 @@ export class TaskScheduler {
|
|
|
524
750
|
if (!silent.skipped) await this.deliverNotifications(task, runInfo);
|
|
525
751
|
this.finishRun(task, taskId, outcome.status, queuedCount);
|
|
526
752
|
|
|
753
|
+
if (this.queue.length > 0 && this.running.size < this.maxConcurrent) {
|
|
754
|
+
const nextItem = this.queue.shift();
|
|
755
|
+
if (nextItem) {
|
|
756
|
+
setImmediate(() => {
|
|
757
|
+
this.runTask(nextItem.taskId).catch(() => {});
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
527
762
|
// 1. Structured LLM Actions (#137)
|
|
528
763
|
if (task.type === 'llm' && outcome.status === 'success' && outcome.output) {
|
|
529
764
|
try {
|
|
@@ -546,9 +781,9 @@ export class TaskScheduler {
|
|
|
546
781
|
}
|
|
547
782
|
}
|
|
548
783
|
|
|
784
|
+
|
|
785
|
+
|
|
549
786
|
// 2. Task Chaining (#137)
|
|
550
|
-
const isSuccess = outcome.status === 'success';
|
|
551
|
-
const isFailure = outcome.status === 'error' || outcome.status === 'timeout';
|
|
552
787
|
const nextTaskId = isSuccess ? task.onSuccess : (isFailure ? task.onFailure : null);
|
|
553
788
|
|
|
554
789
|
if (nextTaskId && typeof nextTaskId === 'string' && nextTaskId.trim()) {
|
package/lib/store.js
CHANGED
|
@@ -239,6 +239,17 @@ export class TaskStore {
|
|
|
239
239
|
onSuccess: task.onSuccess !== undefined ? String(task.onSuccess).trim() : (prev.onSuccess || ''),
|
|
240
240
|
onFailure: task.onFailure !== undefined ? String(task.onFailure).trim() : (prev.onFailure || ''),
|
|
241
241
|
oneShot: task.oneShot !== undefined ? Boolean(task.oneShot) : Boolean(prev.oneShot),
|
|
242
|
+
heartbeatIntervalSeconds: task.heartbeatIntervalSeconds !== undefined ? Number(task.heartbeatIntervalSeconds) : (prev.heartbeatIntervalSeconds || 0),
|
|
243
|
+
gracePeriodSeconds: task.gracePeriodSeconds !== undefined ? Number(task.gracePeriodSeconds) : (prev.gracePeriodSeconds || 300),
|
|
244
|
+
lastPingAt: task.lastPingAt !== undefined ? task.lastPingAt : (prev.lastPingAt || null),
|
|
245
|
+
heartbeatAlerted: task.heartbeatAlerted !== undefined ? Boolean(task.heartbeatAlerted) : (prev.heartbeatAlerted || false),
|
|
246
|
+
preflightType: task.preflightType !== undefined ? String(task.preflightType) : (prev.preflightType || 'none'),
|
|
247
|
+
preflightTarget: task.preflightTarget !== undefined ? String(task.preflightTarget).trim() : (prev.preflightTarget || ''),
|
|
248
|
+
concurrencyGroup: task.concurrencyGroup !== undefined ? String(task.concurrencyGroup).trim() : (prev.concurrencyGroup || 'default'),
|
|
249
|
+
priority: task.priority !== undefined ? Number(task.priority) : (prev.priority || 5),
|
|
250
|
+
selfHealingCommand: task.selfHealingCommand !== undefined ? String(task.selfHealingCommand).trim() : (prev.selfHealingCommand || ''),
|
|
251
|
+
autoDiagnose: task.autoDiagnose !== undefined ? Boolean(task.autoDiagnose) : (prev.autoDiagnose || false),
|
|
252
|
+
agentPreset: task.agentPreset !== undefined ? String(task.agentPreset).trim() : (prev.agentPreset || ''),
|
|
242
253
|
totalTokens: prev.totalTokens || 0,
|
|
243
254
|
totalCostUsd: prev.totalCostUsd || 0,
|
|
244
255
|
createdAt: prev.createdAt || task.createdAt || now,
|
|
@@ -253,6 +264,27 @@ export class TaskStore {
|
|
|
253
264
|
return record;
|
|
254
265
|
}
|
|
255
266
|
|
|
267
|
+
recordHeartbeat(id) {
|
|
268
|
+
const task = this.tasks.get(id);
|
|
269
|
+
if (!task) return null;
|
|
270
|
+
const now = Date.now();
|
|
271
|
+
task.lastPingAt = now;
|
|
272
|
+
task.heartbeatAlerted = false;
|
|
273
|
+
task.updatedAt = now;
|
|
274
|
+
if (task.lastStatus === 'missed') {
|
|
275
|
+
task.lastStatus = 'active';
|
|
276
|
+
}
|
|
277
|
+
this.save();
|
|
278
|
+
const interval = (task.heartbeatIntervalSeconds || 0) + (task.gracePeriodSeconds || 300);
|
|
279
|
+
return {
|
|
280
|
+
ok: true,
|
|
281
|
+
id,
|
|
282
|
+
taskId: id,
|
|
283
|
+
lastPingAt: now,
|
|
284
|
+
nextDeadline: now + interval * 1000,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
256
288
|
delete(id) {
|
|
257
289
|
const existed = this.tasks.delete(id);
|
|
258
290
|
this.history.delete(id);
|
|
@@ -297,6 +329,8 @@ export class TaskStore {
|
|
|
297
329
|
// #43: the failure diagnosis and the prompt change it proposes.
|
|
298
330
|
diagnosis: runInfo.diagnosis ? String(runInfo.diagnosis).slice(0, 1000) : '',
|
|
299
331
|
suggestion: runInfo.suggestion ? String(runInfo.suggestion).slice(0, 2000) : '',
|
|
332
|
+
selfHealing: runInfo.selfHealing || null,
|
|
333
|
+
autoDiagnosis: runInfo.autoDiagnosis ? String(runInfo.autoDiagnosis).slice(0, 1000) : '',
|
|
300
334
|
confidence: runInfo.confidence || '',
|
|
301
335
|
});
|
|
302
336
|
// Keep up to 100 runs in active store; archive older runs to tasks-history-archive.json (#134)
|
package/lib/task-transfer.js
CHANGED
|
@@ -30,6 +30,7 @@ export const PATCHABLE_TASK_FIELDS = [
|
|
|
30
30
|
'maxRetries',
|
|
31
31
|
'retryBackoffMs',
|
|
32
32
|
'permissionPreset',
|
|
33
|
+
'agentPreset',
|
|
33
34
|
'env',
|
|
34
35
|
'cwd',
|
|
35
36
|
'workspaceId',
|
|
@@ -55,6 +56,14 @@ export const PATCHABLE_TASK_FIELDS = [
|
|
|
55
56
|
'kanbanMode',
|
|
56
57
|
'onSuccess',
|
|
57
58
|
'onFailure',
|
|
59
|
+
'heartbeatIntervalSeconds',
|
|
60
|
+
'gracePeriodSeconds',
|
|
61
|
+
'preflightType',
|
|
62
|
+
'preflightTarget',
|
|
63
|
+
'concurrencyGroup',
|
|
64
|
+
'priority',
|
|
65
|
+
'selfHealingCommand',
|
|
66
|
+
'autoDiagnose',
|
|
58
67
|
'channels',
|
|
59
68
|
'template',
|
|
60
69
|
'status',
|