@goodandready/dsh-cron 0.3.15 → 0.3.16
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/lib/api-actions.js +317 -0
- package/lib/api-collection.js +249 -0
- package/lib/api-item.js +190 -0
- package/lib/api.js +12 -747
- package/lib/scheduler-assists.js +142 -0
- package/lib/scheduler-completion.js +413 -0
- package/lib/scheduler-execution.js +30 -738
- package/lib/scheduler-preflight.js +195 -0
- package/package.json +1 -1
package/lib/api.js
CHANGED
|
@@ -1,32 +1,5 @@
|
|
|
1
|
-
import { captureGitDiff, discardGitChanges, applyGitDiff } from './git-diff.js';
|
|
2
|
-
import { applyDoctorFix, revertDoctorFix, diagnoseFailure } from './doctor.js';
|
|
3
1
|
import { handleTaskWebhookRoute } from './api-task-webhook.js';
|
|
4
|
-
import {
|
|
5
|
-
import { sanitizeAdaptiveConfig } from './adaptive.js';
|
|
6
|
-
import { logStreamer } from './log-streamer.js';
|
|
7
|
-
import { approvalManager } from './approval.js';
|
|
8
|
-
import { sanitizeValidators } from './validators.js';
|
|
9
|
-
/**
|
|
10
|
-
* REST surface of the plugin: the /dsh-cron/tasks and /dsh-cron/action route
|
|
11
|
-
* families. Extracted from index.js and split into per-scope handlers so each
|
|
12
|
-
* piece stays reviewable (#97); route contracts and response bodies are
|
|
13
|
-
* unchanged.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { randomUUID, createHash } from 'node:crypto';
|
|
17
|
-
import { parseScheduleExpression } from './scheduler.js';
|
|
18
|
-
import { normalizeTaskType, CODE_EXECUTING_TYPES, EXTERNAL_RUNTIMES } from './runtimes.js';
|
|
19
|
-
import { CHANNEL_IDS, unknownChannelIds } from './channels.js';
|
|
20
|
-
import { applyTaskPatch } from './task-patch.js';
|
|
21
|
-
import { MANAGED_BY_CONFIG, configOwnedMessage } from './config-jobs.js';
|
|
22
|
-
import { sendJson, rejectCrossOrigin, readBody, pickPatchableFields, SCRIPT_CONFIRM_HEADER, redactTaskSecrets, restoreTaskSecrets } from './http-utils.js';
|
|
23
|
-
import {
|
|
24
|
-
validateTaskType,
|
|
25
|
-
buildDuplicateTask,
|
|
26
|
-
RESERVED_TASK_IDS,
|
|
27
|
-
PATCHABLE_TASK_FIELDS,
|
|
28
|
-
isCodeExecutingTask,
|
|
29
|
-
} from './task-transfer.js';
|
|
2
|
+
import { sendJson, rejectCrossOrigin, redactTaskSecrets } from './http-utils.js';
|
|
30
3
|
import {
|
|
31
4
|
handleTaskExport,
|
|
32
5
|
handleTaskImport,
|
|
@@ -39,6 +12,15 @@ import {
|
|
|
39
12
|
handleDryRunTask,
|
|
40
13
|
handleScheduleTimeline,
|
|
41
14
|
} from './api-helpers.js';
|
|
15
|
+
import {
|
|
16
|
+
refuseConfigOwned,
|
|
17
|
+
handleTaskCollection,
|
|
18
|
+
createOrUpdateTask,
|
|
19
|
+
NOT_ALLOWED,
|
|
20
|
+
NOT_FOUND,
|
|
21
|
+
} from './api-collection.js';
|
|
22
|
+
import { handleTaskItem, deleteTask } from './api-item.js';
|
|
23
|
+
import { handleItemPost } from './api-actions.js';
|
|
42
24
|
|
|
43
25
|
export {
|
|
44
26
|
handleTaskExport,
|
|
@@ -49,20 +31,9 @@ export {
|
|
|
49
31
|
handleSchedulePreview,
|
|
50
32
|
handleDryRunTask,
|
|
51
33
|
handleScheduleTimeline,
|
|
34
|
+
refuseConfigOwned,
|
|
52
35
|
};
|
|
53
36
|
|
|
54
|
-
const NOT_FOUND = { ok: false, error: 'Task not found' };
|
|
55
|
-
const NOT_ALLOWED = { ok: false, error: 'Method not allowed' };
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* A task declared in the profile config is owned by that file (#50): the next
|
|
59
|
-
* plugin start would revert any change made here, so every mutating route
|
|
60
|
-
* refuses it and says where the task really lives.
|
|
61
|
-
*/
|
|
62
|
-
function refuseConfigOwned(res, id) {
|
|
63
|
-
sendJson(res, 409, { ok: false, error: configOwnedMessage(id) });
|
|
64
|
-
}
|
|
65
|
-
|
|
66
37
|
/**
|
|
67
38
|
* Shared implementation for the /dsh-cron/tasks... and /dsh-cron/action...
|
|
68
39
|
* route families — the #66 alias is kept alive by the same code path instead
|
|
@@ -141,713 +112,6 @@ export function createCronApiHandler(store, scheduler, collection) {
|
|
|
141
112
|
};
|
|
142
113
|
}
|
|
143
114
|
|
|
144
|
-
// ------------------------------------------------------------- collection
|
|
145
|
-
|
|
146
|
-
async function handleTaskCollection({ store, scheduler, collection, req, res, url, scope, apiToken, ctx }) {
|
|
147
|
-
// The alias family only exists for individual tasks.
|
|
148
|
-
if (scope === 'action' || !collection) {
|
|
149
|
-
sendJson(res, 405, NOT_ALLOWED);
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
if (req.method === 'GET') {
|
|
153
|
-
if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
|
|
154
|
-
listTasks({ store, scheduler, collection, req, res, url });
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
if (req.method === 'POST') {
|
|
158
|
-
await createOrUpdateTask({ store, scheduler, req, res, apiToken, ctx: undefined });
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
sendJson(res, 405, NOT_ALLOWED);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function listTasks({ store, scheduler, collection, req, res, url }) {
|
|
165
|
-
const status = url.searchParams.get('status') || 'all';
|
|
166
|
-
const query = url.searchParams.get('query') || '';
|
|
167
|
-
const list = store.list({ status, query }).map((t) => ({
|
|
168
|
-
...redactTaskSecrets(t),
|
|
169
|
-
running: scheduler.isRunning(t.id),
|
|
170
|
-
runningSince: scheduler.runningSince(t.id),
|
|
171
|
-
}));
|
|
172
|
-
const stats = store.getAggregatedStats();
|
|
173
|
-
const payload = { ok: true, tasks: list, recommendations: collection.recommendations, stats };
|
|
174
|
-
|
|
175
|
-
// Calculate ETag based on payload JSON representation (#134)
|
|
176
|
-
const bodyString = JSON.stringify(payload);
|
|
177
|
-
const etag = 'W/"' + createHash('sha1').update(bodyString).digest('hex') + '"';
|
|
178
|
-
|
|
179
|
-
if (req && req.headers) {
|
|
180
|
-
const ifNoneMatch = req.headers['if-none-match'];
|
|
181
|
-
if (ifNoneMatch && (ifNoneMatch === etag || ifNoneMatch === etag.slice(2))) {
|
|
182
|
-
res.writeHead(304, { 'ETag': etag, 'Cache-Control': 'no-cache' });
|
|
183
|
-
res.end();
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (typeof res.setHeader === 'function') {
|
|
189
|
-
res.setHeader('ETag', etag);
|
|
190
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
191
|
-
}
|
|
192
|
-
sendJson(res, 200, payload);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async function createOrUpdateTask({ store, scheduler, req, res, apiToken, ctx }) {
|
|
196
|
-
if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
|
|
197
|
-
const { body: rawBody, error } = await readBody(req, res);
|
|
198
|
-
if (error) return;
|
|
199
|
-
const existing = rawBody.id ? store.get(rawBody.id) : null;
|
|
200
|
-
const body = existing ? restoreTaskSecrets(rawBody, existing) : rawBody;
|
|
201
|
-
if (!body.title || !body.schedule || !body.prompt) {
|
|
202
|
-
sendJson(res, 400, { ok: false, error: 'Fields title, schedule and prompt are required' });
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
const unknownChannels = unknownChannelIds(body.channels);
|
|
206
|
-
if (unknownChannels.length) {
|
|
207
|
-
sendJson(res, 400, {
|
|
208
|
-
ok: false,
|
|
209
|
-
error: 'Unknown channel ids: ' + unknownChannels.join(', '),
|
|
210
|
-
unknownChannels,
|
|
211
|
-
});
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
const taskType = normalizeTaskType(body.type);
|
|
215
|
-
if ((CODE_EXECUTING_TYPES.includes(taskType) || isCodeExecutingTask(body)) && req.headers[SCRIPT_CONFIRM_HEADER] !== 'script') {
|
|
216
|
-
sendJson(res, 403, { ok: false, error: `Creating code-executing tasks over HTTP requires the x-dsh-cron-confirm: script header` });
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
const typeError = validateTaskType(taskType, body);
|
|
220
|
-
if (typeError) {
|
|
221
|
-
sendJson(res, 400, { ok: false, error: typeError });
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
let parsed;
|
|
225
|
-
try {
|
|
226
|
-
parsed = parseScheduleExpression(body.schedule);
|
|
227
|
-
} catch (err) {
|
|
228
|
-
sendJson(res, 400, { ok: false, error: err.message });
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
// A client-supplied id must not shadow the collection routes under
|
|
232
|
-
// /dsh-cron/tasks, otherwise that task could never be fetched or deleted
|
|
233
|
-
// again (#42 review finding).
|
|
234
|
-
if (body.id && RESERVED_TASK_IDS.includes(String(body.id))) {
|
|
235
|
-
sendJson(res, 400, { ok: false, error: `Task id "${body.id}" is reserved` });
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
// Create-or-update with an existing id is an update too, so the config-owned
|
|
239
|
-
// refusal must apply here as well — otherwise the POST route would be a way
|
|
240
|
-
// around the guard on PATCH (#50 review finding).
|
|
241
|
-
if (body.id) {
|
|
242
|
-
const existing = store.get(body.id);
|
|
243
|
-
if (existing && existing.managedBy === MANAGED_BY_CONFIG) {
|
|
244
|
-
refuseConfigOwned(res, body.id);
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
let task;
|
|
249
|
-
try {
|
|
250
|
-
task = store.set(buildTaskRecord(store, body, taskType, parsed));
|
|
251
|
-
} catch (err) {
|
|
252
|
-
sendJson(res, 500, { ok: false, error: 'Failed to persist task: ' + err.message });
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
if (task.status === 'active') {
|
|
256
|
-
scheduler.scheduleTask(task);
|
|
257
|
-
} else {
|
|
258
|
-
scheduler.pauseTask(task.id);
|
|
259
|
-
}
|
|
260
|
-
sendJson(res, 200, { ok: true, task: redactTaskSecrets(task) });
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/** Merge a create/update payload onto the stored task, if any. */
|
|
264
|
-
function buildTaskRecord(store, body, taskType, parsed) {
|
|
265
|
-
const current = body.id ? store.get(body.id) : null;
|
|
266
|
-
const resolvedStatus = body.status || (current ? current.status : 'active');
|
|
267
|
-
return {
|
|
268
|
-
id: body.id,
|
|
269
|
-
title: body.title,
|
|
270
|
-
schedule: parsed.cronPattern || body.schedule,
|
|
271
|
-
scheduleText: body.scheduleText || parsed.humanText,
|
|
272
|
-
prompt: body.prompt,
|
|
273
|
-
type: taskType,
|
|
274
|
-
delivery: body.delivery || 'current',
|
|
275
|
-
status: resolvedStatus,
|
|
276
|
-
provider: body.provider || undefined,
|
|
277
|
-
model: body.model || undefined,
|
|
278
|
-
fallbackProvider: body.fallbackProvider || undefined,
|
|
279
|
-
fallbackModel: body.fallbackModel || undefined,
|
|
280
|
-
notifyTelegram: body.notifyTelegram !== undefined ? Boolean(body.notifyTelegram) : (current ? current.notifyTelegram : undefined),
|
|
281
|
-
onlyOnFailure: body.onlyOnFailure !== undefined ? Boolean(body.onlyOnFailure) : (current ? current.onlyOnFailure : undefined),
|
|
282
|
-
timeoutSeconds: body.timeoutSeconds !== undefined ? Number(body.timeoutSeconds) : (current ? current.timeoutSeconds : 1800),
|
|
283
|
-
overlapPolicy: body.overlapPolicy || (current ? current.overlapPolicy : 'skip'),
|
|
284
|
-
kanbanMode: body.kanbanMode || (current ? current.kanbanMode : 'none'),
|
|
285
|
-
timezone: body.timezone !== undefined ? String(body.timezone).trim() : (current ? current.timezone : ''),
|
|
286
|
-
misfirePolicy: ['skip', 'runOnce', 'catchUpAll'].includes(body.misfirePolicy) ? body.misfirePolicy : (current ? (current.misfirePolicy || 'skip') : 'skip'),
|
|
287
|
-
maxRetries: body.maxRetries !== undefined ? Math.max(0, Number(body.maxRetries) || 0) : (current ? (Number(current.maxRetries) || 0) : 0),
|
|
288
|
-
retryBackoffMs: body.retryBackoffMs !== undefined ? Math.max(1000, Number(body.retryBackoffMs) || 30000) : (current ? (Number(current.retryBackoffMs) || 30000) : 30000),
|
|
289
|
-
permissionPreset: !EXTERNAL_RUNTIMES.includes(taskType) && ['default', 'read-only', 'workspace-write', 'full'].includes(body.permissionPreset) ? body.permissionPreset : (current && !EXTERNAL_RUNTIMES.includes(taskType) ? (current.permissionPreset || 'default') : 'default'),
|
|
290
|
-
validators: Array.isArray(body.validators) ? sanitizeValidators(body.validators) : (current ? current.validators : undefined),
|
|
291
|
-
...mergeExecutionFields(current, body, parsed),
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// export/import logic moved to ./api-import-export.js
|
|
296
|
-
|
|
297
|
-
// ------------------------------------------------------------ single task
|
|
298
|
-
|
|
299
|
-
async function handleTaskItem({ store, scheduler, req, res, url, id, action, subAction, apiToken, ctx }) {
|
|
300
|
-
if (action === 'heartbeat') {
|
|
301
|
-
handleHeartbeatPing({ store, req, res, taskId: id, apiToken, ctx });
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (req.method === 'POST' && action === 'dry-run') {
|
|
306
|
-
await handleDryRunTask({ store, scheduler, req, res, id, apiToken, ctx });
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (req.method === 'GET') {
|
|
311
|
-
if (action === 'stream') {
|
|
312
|
-
logStreamer.handleSseRequest(id, req, res);
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
|
|
316
|
-
if (action === 'archive') {
|
|
317
|
-
const limit = parseInt(url.searchParams.get('limit') || '50', 10);
|
|
318
|
-
const offset = parseInt(url.searchParams.get('offset') || '0', 10);
|
|
319
|
-
const search = url.searchParams.get('search') || '';
|
|
320
|
-
const result = typeof store.getArchivedRuns === 'function'
|
|
321
|
-
? store.getArchivedRuns(id, { limit, offset, search })
|
|
322
|
-
: { runs: [], total: 0 };
|
|
323
|
-
sendJson(res, 200, { ok: true, taskId: id, ...result });
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
if (action === 'stats') {
|
|
327
|
-
const stats = typeof store.getTaskStats === 'function' ? store.getTaskStats(id) : null;
|
|
328
|
-
sendJson(res, 200, { ok: true, taskId: id, stats });
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
if (action === 'prompt-history') {
|
|
332
|
-
const task = store.get(id);
|
|
333
|
-
if (!task) {
|
|
334
|
-
sendJson(res, 404, NOT_FOUND);
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
sendJson(res, 200, {
|
|
338
|
-
ok: true,
|
|
339
|
-
taskId: id,
|
|
340
|
-
promptVersion: task.promptVersion || 1,
|
|
341
|
-
promptHistory: task.promptHistory || [],
|
|
342
|
-
});
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
if (action === 'history') {
|
|
346
|
-
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
347
|
-
sendJson(res, 200, { ok: true, history: store.getHistory(id, limit) });
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
if (action === 'git-diff') {
|
|
351
|
-
const task = store.get(id);
|
|
352
|
-
if (!task) {
|
|
353
|
-
sendJson(res, 404, NOT_FOUND);
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
const runs = store.getHistory(id, 10);
|
|
357
|
-
const runId = url.searchParams.get('runId') || subAction;
|
|
358
|
-
const targetRun = runId ? runs.find((r) => r.id === runId) : runs[0];
|
|
359
|
-
const diffData = targetRun?.gitDiff || null;
|
|
360
|
-
sendJson(res, 200, {
|
|
361
|
-
ok: true,
|
|
362
|
-
taskId: id,
|
|
363
|
-
runId: targetRun?.id || null,
|
|
364
|
-
gitDiff: diffData,
|
|
365
|
-
});
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
if (action === 'doctor') {
|
|
369
|
-
const task = store.get(id);
|
|
370
|
-
if (!task) {
|
|
371
|
-
sendJson(res, 404, NOT_FOUND);
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
const runs = store.getHistory(id, 5);
|
|
375
|
-
const latestRun = runs[0] || null;
|
|
376
|
-
const reportRun = runs.find((r) => r.doctorReport) || latestRun;
|
|
377
|
-
sendJson(res, 200, {
|
|
378
|
-
ok: true,
|
|
379
|
-
taskId: id,
|
|
380
|
-
doctorReport: reportRun?.doctorReport || null,
|
|
381
|
-
revisions: task.revisions || [],
|
|
382
|
-
previousPrompt: task.previousPrompt || null,
|
|
383
|
-
autoHeal: Boolean(task.autoHeal || task.selfHealing?.autoApply),
|
|
384
|
-
});
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
if (!action) {
|
|
388
|
-
const task = store.get(id);
|
|
389
|
-
if (!task) {
|
|
390
|
-
sendJson(res, 404, NOT_FOUND);
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
sendJson(res, 200, {
|
|
394
|
-
ok: true,
|
|
395
|
-
task: {
|
|
396
|
-
...redactTaskSecrets(task),
|
|
397
|
-
running: scheduler.isRunning(task.id),
|
|
398
|
-
runningSince: scheduler.runningSince(task.id),
|
|
399
|
-
},
|
|
400
|
-
});
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
if (req.method === 'POST') {
|
|
405
|
-
const handled = await handleItemPost({ store, scheduler, req, res, url, id, action, subAction });
|
|
406
|
-
if (handled) return;
|
|
407
|
-
}
|
|
408
|
-
if (req.method === 'PATCH') {
|
|
409
|
-
await patchTask({ store, scheduler, req, res, id });
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
if (req.method === 'DELETE' || (req.method === 'POST' && action === 'delete')) {
|
|
413
|
-
deleteTask({ store, scheduler, res, id });
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
sendJson(res, 405, NOT_ALLOWED);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/** Task actions that are plain POSTs. Returns true when one matched. */
|
|
420
|
-
async function handleItemPost({ store, scheduler, req, res, url, id, action, subAction }) {
|
|
421
|
-
if (action === 'git-diff' && (subAction === 'discard' || subAction === 'revert')) {
|
|
422
|
-
const task = store.get(id);
|
|
423
|
-
if (!task) {
|
|
424
|
-
sendJson(res, 404, NOT_FOUND);
|
|
425
|
-
return true;
|
|
426
|
-
}
|
|
427
|
-
const targetDir = task.cwd;
|
|
428
|
-
if (!targetDir) {
|
|
429
|
-
sendJson(res, 400, { ok: false, error: 'Task does not define a working directory (cwd)' });
|
|
430
|
-
return true;
|
|
431
|
-
}
|
|
432
|
-
const result = await discardGitChanges(targetDir);
|
|
433
|
-
sendJson(res, result.ok ? 200 : 500, result);
|
|
434
|
-
return true;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
if (action === 'git-diff' && subAction === 'apply') {
|
|
438
|
-
const task = store.get(id);
|
|
439
|
-
if (!task) {
|
|
440
|
-
sendJson(res, 404, NOT_FOUND);
|
|
441
|
-
return true;
|
|
442
|
-
}
|
|
443
|
-
const body = await readBody(req, res);
|
|
444
|
-
const bodyData = (body && body.body) || {};
|
|
445
|
-
let patch = bodyData.diff || bodyData.patch;
|
|
446
|
-
if (!patch) {
|
|
447
|
-
const runs = store.getHistory(id, 5);
|
|
448
|
-
const targetRun = runs.find((r) => r.gitDiff?.diff);
|
|
449
|
-
patch = targetRun?.gitDiff?.diff;
|
|
450
|
-
}
|
|
451
|
-
if (!patch) {
|
|
452
|
-
sendJson(res, 400, { ok: false, error: 'No diff patch provided or found in run history' });
|
|
453
|
-
return true;
|
|
454
|
-
}
|
|
455
|
-
const targetDir = task.cwd;
|
|
456
|
-
if (!targetDir) {
|
|
457
|
-
sendJson(res, 400, { ok: false, error: 'Task does not define a working directory (cwd)' });
|
|
458
|
-
return true;
|
|
459
|
-
}
|
|
460
|
-
const result = await applyGitDiff(targetDir, patch);
|
|
461
|
-
sendJson(res, result.ok ? 200 : 500, result);
|
|
462
|
-
return true;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
if ((action === 'doctor' && subAction === 'apply') || action === 'doctor-apply') {
|
|
466
|
-
const task = store.get(id);
|
|
467
|
-
if (!task) {
|
|
468
|
-
sendJson(res, 404, NOT_FOUND);
|
|
469
|
-
return true;
|
|
470
|
-
}
|
|
471
|
-
const body = await readBody(req, res);
|
|
472
|
-
const bodyData = (body && body.body) || {};
|
|
473
|
-
let fix = bodyData.fix || bodyData.doctorReport;
|
|
474
|
-
if (!fix) {
|
|
475
|
-
const runs = store.getHistory(id, 5);
|
|
476
|
-
const reportRun = runs.find((r) => r.doctorReport);
|
|
477
|
-
fix = reportRun?.doctorReport;
|
|
478
|
-
}
|
|
479
|
-
if (!fix || !fix.suggestedFix) {
|
|
480
|
-
sendJson(res, 400, { ok: false, error: 'No doctor fix found to apply' });
|
|
481
|
-
return true;
|
|
482
|
-
}
|
|
483
|
-
try {
|
|
484
|
-
const result = await applyDoctorFix(task, fix, { store, user: bodyData.user || 'operator' });
|
|
485
|
-
if (!result.ok) {
|
|
486
|
-
sendJson(res, 400, { ok: false, error: result.error });
|
|
487
|
-
return true;
|
|
488
|
-
}
|
|
489
|
-
sendJson(res, 200, {
|
|
490
|
-
ok: true,
|
|
491
|
-
taskId: id,
|
|
492
|
-
task: redactTaskSecrets(task),
|
|
493
|
-
revision: result.revision,
|
|
494
|
-
});
|
|
495
|
-
} catch (err) {
|
|
496
|
-
sendJson(res, 500, { ok: false, error: err.message });
|
|
497
|
-
}
|
|
498
|
-
return true;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
if ((action === 'doctor' && subAction === 'revert') || action === 'doctor-revert') {
|
|
502
|
-
const task = store.get(id);
|
|
503
|
-
if (!task) {
|
|
504
|
-
sendJson(res, 404, NOT_FOUND);
|
|
505
|
-
return true;
|
|
506
|
-
}
|
|
507
|
-
const body = await readBody(req, res);
|
|
508
|
-
const bodyData = (body && body.body) || {};
|
|
509
|
-
try {
|
|
510
|
-
const result = await revertDoctorFix(task, { store, user: bodyData.user || 'operator' });
|
|
511
|
-
if (!result.ok) {
|
|
512
|
-
sendJson(res, 400, { ok: false, error: result.error });
|
|
513
|
-
return true;
|
|
514
|
-
}
|
|
515
|
-
sendJson(res, 200, {
|
|
516
|
-
ok: true,
|
|
517
|
-
taskId: id,
|
|
518
|
-
task: redactTaskSecrets(task),
|
|
519
|
-
revertedCode: result.revertedCode,
|
|
520
|
-
});
|
|
521
|
-
} catch (err) {
|
|
522
|
-
sendJson(res, 500, { ok: false, error: err.message });
|
|
523
|
-
}
|
|
524
|
-
return true;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
if ((action === 'doctor' && subAction === 'diagnose') || action === 'doctor-diagnose') {
|
|
528
|
-
const task = store.get(id);
|
|
529
|
-
if (!task) {
|
|
530
|
-
sendJson(res, 404, NOT_FOUND);
|
|
531
|
-
return true;
|
|
532
|
-
}
|
|
533
|
-
const body = await readBody(req, res);
|
|
534
|
-
const bodyData = (body && body.body) || {};
|
|
535
|
-
const runId = bodyData.runId;
|
|
536
|
-
const runs = store.getHistory(id, 10);
|
|
537
|
-
const targetRun = runId ? runs.find((r) => r.id === runId) : runs[0];
|
|
538
|
-
if (!targetRun) {
|
|
539
|
-
sendJson(res, 404, { ok: false, error: 'No run history found to diagnose' });
|
|
540
|
-
return true;
|
|
541
|
-
}
|
|
542
|
-
try {
|
|
543
|
-
const doctorReport = await diagnoseFailure({
|
|
544
|
-
task,
|
|
545
|
-
runInfo: targetRun,
|
|
546
|
-
ask: scheduler?.askModel,
|
|
547
|
-
});
|
|
548
|
-
targetRun.doctorReport = doctorReport;
|
|
549
|
-
sendJson(res, 200, { ok: true, taskId: id, doctorReport });
|
|
550
|
-
} catch (err) {
|
|
551
|
-
sendJson(res, 500, { ok: false, error: err.message });
|
|
552
|
-
}
|
|
553
|
-
return true;
|
|
554
|
-
}
|
|
555
|
-
if (action === 'prompt-rollback') {
|
|
556
|
-
const task = store.get(id);
|
|
557
|
-
if (!task) {
|
|
558
|
-
sendJson(res, 404, NOT_FOUND);
|
|
559
|
-
return true;
|
|
560
|
-
}
|
|
561
|
-
const body = await readBodyJson(req).catch(() => ({}));
|
|
562
|
-
const targetVersion = Number(body.version || url.searchParams.get('version'));
|
|
563
|
-
if (!targetVersion || targetVersion < 1) {
|
|
564
|
-
sendJson(res, 400, { ok: false, error: 'A valid target prompt version is required' });
|
|
565
|
-
return true;
|
|
566
|
-
}
|
|
567
|
-
try {
|
|
568
|
-
rollbackPromptVersion(task, targetVersion);
|
|
569
|
-
store.set(task);
|
|
570
|
-
sendJson(res, 200, {
|
|
571
|
-
ok: true,
|
|
572
|
-
task: redactTaskSecrets(task),
|
|
573
|
-
promptVersion: task.promptVersion,
|
|
574
|
-
promptHistory: task.promptHistory,
|
|
575
|
-
});
|
|
576
|
-
} catch (err) {
|
|
577
|
-
sendJson(res, 400, { ok: false, error: err.message });
|
|
578
|
-
}
|
|
579
|
-
return true;
|
|
580
|
-
}
|
|
581
|
-
if (action === 'retry-delivery') {
|
|
582
|
-
const task = store.get(id);
|
|
583
|
-
if (!task) {
|
|
584
|
-
sendJson(res, 404, NOT_FOUND);
|
|
585
|
-
return true;
|
|
586
|
-
}
|
|
587
|
-
const body = await readBodyJson(req).catch(() => ({}));
|
|
588
|
-
const runId = body.runId || url.searchParams.get('runId');
|
|
589
|
-
const runs = store.history.get(id) || [];
|
|
590
|
-
const run = runId ? runs.find(r => r.id === runId) : runs[0];
|
|
591
|
-
if (!run) {
|
|
592
|
-
sendJson(res, 404, { ok: false, error: 'Run not found' });
|
|
593
|
-
return true;
|
|
594
|
-
}
|
|
595
|
-
const delivery = await scheduler.deliverNotifications(task, run);
|
|
596
|
-
run.delivery = delivery;
|
|
597
|
-
if (Array.isArray(run.stages)) {
|
|
598
|
-
const deliveryStage = run.stages.find(s => s.name === 'delivery');
|
|
599
|
-
if (deliveryStage) {
|
|
600
|
-
deliveryStage.status = delivery?.status || 'delivered';
|
|
601
|
-
deliveryStage.delivered = delivery?.delivered;
|
|
602
|
-
deliveryStage.failures = delivery?.failures;
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
store.scheduleSave();
|
|
606
|
-
sendJson(res, 200, { ok: true, delivery, run });
|
|
607
|
-
return true;
|
|
608
|
-
}
|
|
609
|
-
if (action === 'approve' || action === 'reject') {
|
|
610
|
-
const task = store.get(id);
|
|
611
|
-
if (!task) {
|
|
612
|
-
sendJson(res, 404, NOT_FOUND);
|
|
613
|
-
return true;
|
|
614
|
-
}
|
|
615
|
-
const body = await readBodyJson(req).catch(() => ({}));
|
|
616
|
-
const proposalId = body.proposalId || url.searchParams.get('proposalId');
|
|
617
|
-
const proposal = proposalId
|
|
618
|
-
? approvalManager.getProposal(proposalId)
|
|
619
|
-
: approvalManager.getPendingProposalForTask(id);
|
|
620
|
-
|
|
621
|
-
if (!proposal) {
|
|
622
|
-
sendJson(res, 404, { ok: false, error: 'No pending approval proposal found for this task' });
|
|
623
|
-
return true;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
const decision = action === 'approve' ? 'approve' : 'reject';
|
|
627
|
-
const result = approvalManager.decide(proposal.id, decision, {
|
|
628
|
-
decidedBy: body.decidedBy || 'web-user',
|
|
629
|
-
reason: body.reason || '',
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
if (!result.ok) {
|
|
633
|
-
sendJson(res, 400, result);
|
|
634
|
-
return true;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
if (decision === 'approve') {
|
|
638
|
-
scheduler.runTask(id, { approvedBy: body.decidedBy || 'web-user' }).catch(() => {});
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
sendJson(res, 200, { ok: true, proposal: result.proposal });
|
|
642
|
-
return true;
|
|
643
|
-
}
|
|
644
|
-
if (action === 'resolve-uncertain') {
|
|
645
|
-
const task = store.get(id);
|
|
646
|
-
if (!task) {
|
|
647
|
-
sendJson(res, 404, NOT_FOUND);
|
|
648
|
-
return true;
|
|
649
|
-
}
|
|
650
|
-
const body = await readBody(req).then(b => JSON.parse(b || '{}')).catch(() => ({}));
|
|
651
|
-
const runId = body.runId || url.searchParams.get('runId');
|
|
652
|
-
if (!runId) {
|
|
653
|
-
sendJson(res, 400, { ok: false, error: 'runId is required' });
|
|
654
|
-
return true;
|
|
655
|
-
}
|
|
656
|
-
const result = store.durableJournal.resolveUncertain(id, runId, {
|
|
657
|
-
decision: body.decision || 'mark_failed',
|
|
658
|
-
note: body.note || '',
|
|
659
|
-
resolvedBy: body.resolvedBy || 'user',
|
|
660
|
-
});
|
|
661
|
-
if (!result.ok) {
|
|
662
|
-
sendJson(res, 400, result);
|
|
663
|
-
return true;
|
|
664
|
-
}
|
|
665
|
-
sendJson(res, 200, result);
|
|
666
|
-
return true;
|
|
667
|
-
}
|
|
668
|
-
if (action === 'run') {
|
|
669
|
-
if (!store.get(id)) {
|
|
670
|
-
sendJson(res, 404, NOT_FOUND);
|
|
671
|
-
return true;
|
|
672
|
-
}
|
|
673
|
-
await scheduler.triggerManualRun(id);
|
|
674
|
-
sendJson(res, 200, { ok: true, task: redactTaskSecrets(store.get(id)) });
|
|
675
|
-
return true;
|
|
676
|
-
}
|
|
677
|
-
if (action === 'duplicate') {
|
|
678
|
-
const source = store.get(id);
|
|
679
|
-
if (!source) {
|
|
680
|
-
sendJson(res, 404, NOT_FOUND);
|
|
681
|
-
return true;
|
|
682
|
-
}
|
|
683
|
-
const copy = buildDuplicateTask(source, { id: randomUUID() });
|
|
684
|
-
try {
|
|
685
|
-
store.set(copy);
|
|
686
|
-
} catch (err) {
|
|
687
|
-
sendJson(res, 500, { ok: false, error: 'Failed to persist duplicate task: ' + err.message });
|
|
688
|
-
return true;
|
|
689
|
-
}
|
|
690
|
-
// A copy starts paused: it must never fire on its own before the user
|
|
691
|
-
// reviews the schedule (a duplicated one-shot may point at a past time).
|
|
692
|
-
scheduler.pauseTask(copy.id);
|
|
693
|
-
sendJson(res, 200, { ok: true, task: redactTaskSecrets(copy) });
|
|
694
|
-
return true;
|
|
695
|
-
}
|
|
696
|
-
if (action === 'pause' || action === 'resume') {
|
|
697
|
-
if (store.get(id) && store.get(id).managedBy === MANAGED_BY_CONFIG) {
|
|
698
|
-
refuseConfigOwned(res, id);
|
|
699
|
-
return true;
|
|
700
|
-
}
|
|
701
|
-
const task = action === 'pause' ? scheduler.pauseTask(id) : scheduler.resumeTask(id);
|
|
702
|
-
if (!task) {
|
|
703
|
-
sendJson(res, 404, NOT_FOUND);
|
|
704
|
-
return true;
|
|
705
|
-
}
|
|
706
|
-
sendJson(res, 200, { ok: true, task: redactTaskSecrets(task) });
|
|
707
|
-
return true;
|
|
708
|
-
}
|
|
709
|
-
if (action === 'toggle' || (!action && url.searchParams.get('action') === 'toggle')) {
|
|
710
|
-
const stored = store.get(id);
|
|
711
|
-
if (!stored) {
|
|
712
|
-
sendJson(res, 404, NOT_FOUND);
|
|
713
|
-
return true;
|
|
714
|
-
}
|
|
715
|
-
if (stored.managedBy === MANAGED_BY_CONFIG) {
|
|
716
|
-
refuseConfigOwned(res, id);
|
|
717
|
-
return true;
|
|
718
|
-
}
|
|
719
|
-
const task = scheduler.toggleTask(id);
|
|
720
|
-
sendJson(res, 200, { ok: true, task: redactTaskSecrets(task) });
|
|
721
|
-
return true;
|
|
722
|
-
}
|
|
723
|
-
return false;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
async function patchTask({ store, scheduler, req, res, id }) {
|
|
727
|
-
const current = store.get(id);
|
|
728
|
-
if (!current) {
|
|
729
|
-
sendJson(res, 404, NOT_FOUND);
|
|
730
|
-
return;
|
|
731
|
-
}
|
|
732
|
-
if (current.managedBy === MANAGED_BY_CONFIG) {
|
|
733
|
-
refuseConfigOwned(res, id);
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
736
|
-
const { body, error } = await readBody(req, res);
|
|
737
|
-
if (error) return;
|
|
738
|
-
const result = applyTaskPatch({
|
|
739
|
-
store,
|
|
740
|
-
scheduler,
|
|
741
|
-
id,
|
|
742
|
-
body,
|
|
743
|
-
allowCodeSwitch: req.headers[SCRIPT_CONFIRM_HEADER] === 'script',
|
|
744
|
-
});
|
|
745
|
-
if (!result.ok) {
|
|
746
|
-
const status = result.status || (result.notFound ? 404 : (result.needsConfirmation ? 403 : 400));
|
|
747
|
-
const error = result.needsConfirmation
|
|
748
|
-
? `Switching a task to the ${normalizeTaskType(body.type)} type over HTTP requires the ${SCRIPT_CONFIRM_HEADER}: script header`
|
|
749
|
-
: result.error;
|
|
750
|
-
sendJson(res, status, {
|
|
751
|
-
ok: false,
|
|
752
|
-
error,
|
|
753
|
-
...(result.unknownChannels ? { unknownChannels: result.unknownChannels } : {}),
|
|
754
|
-
});
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
sendJson(res, 200, { ok: true, task: redactTaskSecrets(result.task) });
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
function deleteTask({ store, scheduler, res, id }) {
|
|
761
|
-
const current = store.get(id);
|
|
762
|
-
if (current && current.managedBy === MANAGED_BY_CONFIG) {
|
|
763
|
-
refuseConfigOwned(res, id);
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
766
|
-
if (current) {
|
|
767
|
-
if (scheduler && typeof scheduler.removeTask === 'function') {
|
|
768
|
-
scheduler.removeTask(id);
|
|
769
|
-
} else if (scheduler) {
|
|
770
|
-
scheduler.pauseTask(id);
|
|
771
|
-
}
|
|
772
|
-
try {
|
|
773
|
-
store.delete(id);
|
|
774
|
-
} catch (err) {
|
|
775
|
-
sendJson(res, 500, { ok: false, error: 'Failed to delete task: ' + err.message });
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
sendJson(res, 200, { ok: true });
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
/**
|
|
783
|
-
* Runtime and execution settings of a task, merged onto the stored one. Split
|
|
784
|
-
* out of buildTaskRecord so both stay readable.
|
|
785
|
-
*/
|
|
786
|
-
function mergeExecutionFields(current, body, parsed) {
|
|
787
|
-
return {
|
|
788
|
-
env: body.env && typeof body.env === 'object' ? body.env : (current ? current.env : undefined),
|
|
789
|
-
cwd: body.cwd !== undefined ? String(body.cwd).trim() : (current ? current.cwd : ''),
|
|
790
|
-
channels: Array.isArray(body.channels)
|
|
791
|
-
? body.channels.filter((c) => CHANNEL_IDS.includes(c))
|
|
792
|
-
: (current ? current.channels : undefined),
|
|
793
|
-
template: body.template !== undefined ? String(body.template) : (current ? current.template : ''),
|
|
794
|
-
silentRule: body.silentRule !== undefined ? String(body.silentRule) : (current ? current.silentRule : ''),
|
|
795
|
-
inspectOnFailure: body.inspectOnFailure !== undefined ? Boolean(body.inspectOnFailure) : (current ? Boolean(current.inspectOnFailure) : false),
|
|
796
|
-
workspaceId: body.workspaceId !== undefined ? String(body.workspaceId).trim() : (current ? current.workspaceId : ''),
|
|
797
|
-
worktree: body.worktree !== undefined ? Boolean(body.worktree) : (current ? Boolean(current.worktree) : false),
|
|
798
|
-
keepWorktree: body.keepWorktree !== undefined ? Boolean(body.keepWorktree) : (current ? Boolean(current.keepWorktree) : false),
|
|
799
|
-
httpMethod: body.httpMethod ? String(body.httpMethod).toUpperCase() : (current ? current.httpMethod : 'GET'),
|
|
800
|
-
httpUrl: body.httpUrl !== undefined ? String(body.httpUrl).trim() : (current ? current.httpUrl : ''),
|
|
801
|
-
httpHeaders: body.httpHeaders !== undefined ? body.httpHeaders : (current ? current.httpHeaders : undefined),
|
|
802
|
-
httpBody: body.httpBody !== undefined ? String(body.httpBody) : (current ? current.httpBody : ''),
|
|
803
|
-
sshProfileId: body.sshProfileId !== undefined ? String(body.sshProfileId).trim() : (current ? current.sshProfileId : ''),
|
|
804
|
-
sshTarget: body.sshTarget !== undefined ? String(body.sshTarget).trim() : (current ? current.sshTarget : ''),
|
|
805
|
-
sshPort: body.sshPort !== undefined ? Number(body.sshPort) || 0 : (current ? current.sshPort : 0),
|
|
806
|
-
sshKeyPath: body.sshKeyPath !== undefined ? String(body.sshKeyPath).trim() : (current ? current.sshKeyPath : ''),
|
|
807
|
-
dockerImage: body.dockerImage !== undefined ? String(body.dockerImage).trim() : (current ? current.dockerImage : ''),
|
|
808
|
-
pythonPath: body.pythonPath !== undefined ? String(body.pythonPath).trim() : (current ? current.pythonPath : ''),
|
|
809
|
-
nodePath: body.nodePath !== undefined ? String(body.nodePath).trim() : (current ? current.nodePath : ''),
|
|
810
|
-
skillName: body.skillName !== undefined ? String(body.skillName).trim() : (current ? current.skillName : ''),
|
|
811
|
-
workflowName: body.workflowName !== undefined ? String(body.workflowName).trim() : (current ? current.workflowName : ''),
|
|
812
|
-
oneShot: Boolean((parsed && parsed.isOneShot) || (body && body.oneShot)),
|
|
813
|
-
targetTimestamp: parsed && parsed.isOneShot
|
|
814
|
-
? (current && current.schedule === (parsed.cronPattern || body.schedule) ? (current.targetTimestamp || parsed.targetTimestamp) : parsed.targetTimestamp)
|
|
815
|
-
: (current ? current.targetTimestamp : undefined),
|
|
816
|
-
costLimitUsd: body.costLimitUsd !== undefined ? (body.costLimitUsd === null ? null : (Number(body.costLimitUsd) > 0 ? Number(body.costLimitUsd) : null)) : (current ? current.costLimitUsd : undefined),
|
|
817
|
-
dailyCostLimitUsd: body.dailyCostLimitUsd !== undefined ? (body.dailyCostLimitUsd === null ? null : (Number(body.dailyCostLimitUsd) > 0 ? Number(body.dailyCostLimitUsd) : null)) : (current ? current.dailyCostLimitUsd : undefined),
|
|
818
|
-
tokenLimit: body.tokenLimit !== undefined ? (body.tokenLimit === null ? null : (Number(body.tokenLimit) > 0 ? Number(body.tokenLimit) : null)) : (current ? current.tokenLimit : undefined),
|
|
819
|
-
pausedReason: body.pausedReason !== undefined ? (body.pausedReason ? String(body.pausedReason) : null) : (current ? current.pausedReason : undefined),
|
|
820
|
-
agentPreset: body.agentPreset !== undefined ? String(body.agentPreset).trim() : (current ? (current.agentPreset || '') : ''),
|
|
821
|
-
targetSessionId: body.targetSessionId !== undefined ? String(body.targetSessionId).trim() : (current ? (current.targetSessionId || '') : ''),
|
|
822
|
-
targetSessionReset: body.targetSessionReset !== undefined ? String(body.targetSessionReset).trim() : (current ? (current.targetSessionReset || 'never') : 'never'),
|
|
823
|
-
sessionTitle: body.sessionTitle !== undefined ? String(body.sessionTitle).trim() : (current ? (current.sessionTitle || '') : ''),
|
|
824
|
-
webhookEnabled: body.webhookEnabled !== undefined
|
|
825
|
-
? Boolean(body.webhookEnabled)
|
|
826
|
-
: (body.webhookTrigger?.enabled !== undefined ? Boolean(body.webhookTrigger.enabled) : (current ? Boolean(current.webhookEnabled || current.webhookTrigger?.enabled) : false)),
|
|
827
|
-
webhookToken: body.webhookToken !== undefined
|
|
828
|
-
? String(body.webhookToken).trim()
|
|
829
|
-
: (body.webhookTrigger?.token ? String(body.webhookTrigger.token).trim() : (current ? (current.webhookToken || current.webhookTrigger?.token || '') : '')),
|
|
830
|
-
webhookSecret: body.webhookSecret !== undefined
|
|
831
|
-
? String(body.webhookSecret).trim()
|
|
832
|
-
: (body.webhookTrigger?.secret ? String(body.webhookTrigger.secret).trim() : (current ? (current.webhookSecret || current.webhookTrigger?.secret || '') : '')),
|
|
833
|
-
webhookDebounceMs: body.webhookDebounceMs !== undefined
|
|
834
|
-
? (Number(body.webhookDebounceMs) || 0)
|
|
835
|
-
: (body.webhookTrigger?.debounceMs !== undefined ? (Number(body.webhookTrigger.debounceMs) || 0) : (current ? (Number(current.webhookDebounceMs || current.webhookTrigger?.debounceMs) || 0) : 0)),
|
|
836
|
-
onSuccess: body.onSuccess !== undefined ? String(body.onSuccess).trim() : (current ? (current.onSuccess || '') : ''),
|
|
837
|
-
onFailure: body.onFailure !== undefined ? String(body.onFailure).trim() : (current ? (current.onFailure || '') : ''),
|
|
838
|
-
heartbeatIntervalSeconds: body.heartbeatIntervalSeconds !== undefined ? Number(body.heartbeatIntervalSeconds) : (current ? (Number(current.heartbeatIntervalSeconds) || 0) : 0),
|
|
839
|
-
gracePeriodSeconds: body.gracePeriodSeconds !== undefined ? Number(body.gracePeriodSeconds) : (current ? (Number(current.gracePeriodSeconds) || 300) : 300),
|
|
840
|
-
preflightType: body.preflightType !== undefined ? String(body.preflightType).trim().toLowerCase() : (current ? (current.preflightType || 'none') : 'none'),
|
|
841
|
-
preflightTarget: body.preflightTarget !== undefined ? String(body.preflightTarget).trim() : (current ? (current.preflightTarget || '') : ''),
|
|
842
|
-
priority: body.priority !== undefined ? (Number(body.priority) || 5) : (current ? (Number(current.priority) || 5) : 5),
|
|
843
|
-
concurrencyGroup: body.concurrencyGroup !== undefined ? String(body.concurrencyGroup).trim() : (current ? (current.concurrencyGroup || 'default') : 'default'),
|
|
844
|
-
selfHealingCommand: body.selfHealingCommand !== undefined ? String(body.selfHealingCommand).trim() : (current ? (current.selfHealingCommand || '') : ''),
|
|
845
|
-
autoDiagnose: body.autoDiagnose !== undefined ? Boolean(body.autoDiagnose) : (current ? Boolean(current.autoDiagnose) : false),
|
|
846
|
-
adaptive: body.adaptive !== undefined ? sanitizeAdaptiveConfig(body.adaptive) : (current ? current.adaptive : undefined),
|
|
847
|
-
adaptiveState: current ? current.adaptiveState : undefined,
|
|
848
|
-
};
|
|
849
|
-
}
|
|
850
|
-
|
|
851
115
|
// --------------------------------------------------------- external surface
|
|
852
116
|
|
|
853
117
|
/**
|
|
@@ -911,3 +175,4 @@ export async function handleExternalTaskRequest({ store, scheduler, req, res, ur
|
|
|
911
175
|
}
|
|
912
176
|
|
|
913
177
|
// webhook and helper endpoints moved to ./api-webhook.js and ./api-helpers.js
|
|
178
|
+
|