@dcrays/scheduled-task 0.1.4 → 0.1.6-beta.1
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/cordis.patch.yml +0 -1
- package/package.json +2 -2
- package/plugin/index.d.ts +1 -1
- package/plugin/index.js +344 -89
- package/src/cron.js +3 -1
- package/src/manager.d.ts +14 -0
- package/src/manager.js +166 -64
- package/src/openclaw-sqlite.d.ts +2 -0
- package/src/openclaw-sqlite.js +3 -3
- package/src/output.d.ts +2 -0
- package/src/output.js +26 -0
- package/src/repository.d.ts +3 -0
- package/src/repository.js +28 -0
- package/src/runtime-environment.d.ts +15 -2
- package/src/runtime-environment.js +117 -7
package/plugin/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { createRequire as __mobookCreateRequire } from 'node:module'; const require = __mobookCreateRequire(import.meta.url);
|
|
2
2
|
|
|
3
|
-
// dist
|
|
3
|
+
// dist/plugin/index.js
|
|
4
4
|
import { existsSync as existsSync3 } from "node:fs";
|
|
5
|
-
import
|
|
5
|
+
import path5 from "node:path";
|
|
6
6
|
import { Service } from "@deepseek-ai/cordis";
|
|
7
7
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
8
8
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
9
9
|
import z from "@deepseek-ai/schemastery";
|
|
10
10
|
|
|
11
|
-
// dist
|
|
11
|
+
// dist/src/cron.js
|
|
12
12
|
var FIELD_COUNT = 5;
|
|
13
13
|
var MAX_SEARCH_DAYS = 366 * 8;
|
|
14
14
|
var GREGORIAN_CYCLE_DAYS = 146097;
|
|
@@ -34,7 +34,7 @@ function parseInteger(value, label) {
|
|
|
34
34
|
}
|
|
35
35
|
function parseField(raw, min, max, label, normalize) {
|
|
36
36
|
const values = /* @__PURE__ */ new Set();
|
|
37
|
-
const wildcard = raw === "*"
|
|
37
|
+
const wildcard = raw === "*";
|
|
38
38
|
if (raw.length === 0)
|
|
39
39
|
throw new CronExpressionError(`${label} is empty`);
|
|
40
40
|
for (const segment of raw.split(",")) {
|
|
@@ -208,10 +208,10 @@ function nextCronOccurrence(afterEpochMs, cron, timeZone) {
|
|
|
208
208
|
throw new CronExpressionError("cron expression has no occurrence within the next eight years");
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
// dist
|
|
211
|
+
// dist/src/manager.js
|
|
212
212
|
import { createHash } from "node:crypto";
|
|
213
213
|
|
|
214
|
-
// dist
|
|
214
|
+
// dist/src/protocol.js
|
|
215
215
|
var CRON_CREATE_TYPE = "dsh/cron.create";
|
|
216
216
|
var CRON_LIST_TYPE = "dsh/cron.list";
|
|
217
217
|
var CronAutomationInputError = class extends Error {
|
|
@@ -302,7 +302,7 @@ function parseCronEditorRequest(message) {
|
|
|
302
302
|
return { id, text };
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
// dist
|
|
305
|
+
// dist/src/repository.js
|
|
306
306
|
import fs from "node:fs";
|
|
307
307
|
import path from "node:path";
|
|
308
308
|
import { DatabaseSync } from "node:sqlite";
|
|
@@ -579,6 +579,11 @@ var CronAutomationRepository = class {
|
|
|
579
579
|
} catch {
|
|
580
580
|
}
|
|
581
581
|
}
|
|
582
|
+
hasLegacyMigration(migrationId) {
|
|
583
|
+
this.assertOpen();
|
|
584
|
+
const table = this.database.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'cron_migrations'").get();
|
|
585
|
+
return Boolean(table && this.database.prepare("SELECT 1 FROM cron_migrations WHERE migration_id = ?").get(migrationId));
|
|
586
|
+
}
|
|
582
587
|
async load() {
|
|
583
588
|
this.assertOpen();
|
|
584
589
|
const rows = this.database.prepare(`
|
|
@@ -634,6 +639,24 @@ var CronAutomationRepository = class {
|
|
|
634
639
|
return [];
|
|
635
640
|
return this.database.prepare("SELECT source, job_id FROM cron_imported_jobs").all().map((row) => ({ source: row.source, jobId: row.job_id }));
|
|
636
641
|
}
|
|
642
|
+
/** Only a verified complete source snapshot may authorize these deletions. */
|
|
643
|
+
async removeMissingImportedJobs(source, presentIds) {
|
|
644
|
+
this.assertWritable();
|
|
645
|
+
if (!source.trim())
|
|
646
|
+
throw new Error("cron import source must be non-empty");
|
|
647
|
+
return this.writeTransaction(() => {
|
|
648
|
+
const rows = this.database.prepare(`
|
|
649
|
+
SELECT jobs.job_id FROM cron_jobs AS jobs
|
|
650
|
+
INNER JOIN cron_imported_jobs AS imported ON imported.job_id = jobs.job_id
|
|
651
|
+
WHERE jobs.store_key = ? AND imported.source = ?
|
|
652
|
+
`).all(STORE_KEY, source);
|
|
653
|
+
const removed = rows.map((row) => row.job_id).filter((id) => !presentIds.has(id));
|
|
654
|
+
const statement = this.database.prepare("DELETE FROM cron_jobs WHERE store_key = ? AND job_id = ?");
|
|
655
|
+
for (const id of removed)
|
|
656
|
+
statement.run(STORE_KEY, id);
|
|
657
|
+
return removed;
|
|
658
|
+
});
|
|
659
|
+
}
|
|
637
660
|
async migrateLegacy(migrationId, incoming, importedJobs) {
|
|
638
661
|
this.assertWritable();
|
|
639
662
|
const decoded = uniqueJobs(incoming);
|
|
@@ -714,8 +737,9 @@ var CronAutomationRepository = class {
|
|
|
714
737
|
}
|
|
715
738
|
};
|
|
716
739
|
|
|
717
|
-
// dist
|
|
740
|
+
// dist/src/manager.js
|
|
718
741
|
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
742
|
+
var DELIVERY_RETRY_DELAY_MS = 3e4;
|
|
719
743
|
function automationId(agentId, requestId) {
|
|
720
744
|
return `cron-${createHash("sha256").update(`${agentId}\0${requestId}`).digest("hex").slice(0, 24)}`;
|
|
721
745
|
}
|
|
@@ -802,15 +826,25 @@ var CronAutomationManager = class {
|
|
|
802
826
|
options;
|
|
803
827
|
jobs = /* @__PURE__ */ new Map();
|
|
804
828
|
blockedAgents = /* @__PURE__ */ new Set();
|
|
829
|
+
blockedUntil = /* @__PURE__ */ new Map();
|
|
830
|
+
activeJobs = /* @__PURE__ */ new Set();
|
|
831
|
+
inflight = /* @__PURE__ */ new Set();
|
|
805
832
|
now;
|
|
806
833
|
started;
|
|
807
834
|
tail = Promise.resolve();
|
|
835
|
+
dispatchLoop = Promise.resolve();
|
|
808
836
|
timer;
|
|
809
837
|
stopping = false;
|
|
838
|
+
schedulingPaused;
|
|
810
839
|
constructor(repository, options) {
|
|
811
840
|
this.repository = repository;
|
|
812
841
|
this.options = options;
|
|
813
842
|
this.now = options.now ?? Date.now;
|
|
843
|
+
this.schedulingPaused = options.startPaused ?? false;
|
|
844
|
+
}
|
|
845
|
+
resumeScheduling() {
|
|
846
|
+
this.schedulingPaused = false;
|
|
847
|
+
this.arm();
|
|
814
848
|
}
|
|
815
849
|
start() {
|
|
816
850
|
return this.started ??= this.initialize();
|
|
@@ -842,6 +876,8 @@ var CronAutomationManager = class {
|
|
|
842
876
|
clearTimeout(this.timer);
|
|
843
877
|
this.timer = void 0;
|
|
844
878
|
try {
|
|
879
|
+
await this.dispatchLoop.catch(() => void 0);
|
|
880
|
+
await Promise.all([...this.inflight]);
|
|
845
881
|
await this.tail;
|
|
846
882
|
} finally {
|
|
847
883
|
this.repository.close();
|
|
@@ -1012,31 +1048,12 @@ var CronAutomationManager = class {
|
|
|
1012
1048
|
}
|
|
1013
1049
|
async runOnceStatus(jobId) {
|
|
1014
1050
|
await this.start();
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
if (!hasDshSession(job))
|
|
1022
|
-
return "delivery_unavailable";
|
|
1023
|
-
const occurrenceAtMs = this.now();
|
|
1024
|
-
job.state.runningAtMs = occurrenceAtMs;
|
|
1025
|
-
job.updatedAtMs = occurrenceAtMs;
|
|
1026
|
-
await this.repository.upsert([job]);
|
|
1027
|
-
const delivered = await this.deliver(job, occurrenceAtMs);
|
|
1028
|
-
delete job.state.runningAtMs;
|
|
1029
|
-
job.state.lastRunAtMs = occurrenceAtMs;
|
|
1030
|
-
job.state.lastRunStatus = delivered ? "ok" : "skipped";
|
|
1031
|
-
job.updatedAtMs = this.now();
|
|
1032
|
-
await this.repository.upsert([job]);
|
|
1033
|
-
if (delivered) {
|
|
1034
|
-
this.options.onDispatched?.(cloneJob(job), new Date(occurrenceAtMs).toISOString());
|
|
1035
|
-
return "dispatched";
|
|
1036
|
-
}
|
|
1037
|
-
this.options.onChanged?.(cloneJob(job));
|
|
1038
|
-
return "delivery_unavailable";
|
|
1039
|
-
});
|
|
1051
|
+
const claimed = await this.enqueue(() => this.claimManualRun(jobId));
|
|
1052
|
+
if (claimed.kind === "status")
|
|
1053
|
+
return claimed.status;
|
|
1054
|
+
const delivered = await this.deliverTracked(claimed.job, claimed.occurrenceAtMs);
|
|
1055
|
+
await this.enqueue(() => this.finalizeManualRun(claimed.job.id, claimed.occurrenceAtMs, delivered));
|
|
1056
|
+
return delivered ? "dispatched" : "delivery_unavailable";
|
|
1040
1057
|
}
|
|
1041
1058
|
async runOnce(jobId) {
|
|
1042
1059
|
return await this.runOnceStatus(jobId) === "dispatched";
|
|
@@ -1047,10 +1064,25 @@ var CronAutomationManager = class {
|
|
|
1047
1064
|
return [...this.jobs.values()].filter((job) => agentId === void 0 || dshAgentIdFor(job) === agentId).sort((left, right) => left.createdAtMs - right.createdAtMs || left.id.localeCompare(right.id)).map(cloneJob);
|
|
1048
1065
|
}
|
|
1049
1066
|
notifyAgentAvailable(agentId) {
|
|
1050
|
-
|
|
1051
|
-
|
|
1067
|
+
this.blockedAgents.delete(agentId);
|
|
1068
|
+
this.blockedUntil.delete(agentId);
|
|
1052
1069
|
this.requestDispatch();
|
|
1053
1070
|
}
|
|
1071
|
+
/** Reflect deletions only for ids recorded in this source's import ledger. */
|
|
1072
|
+
async reconcileImportedDeletions(source, presentIds) {
|
|
1073
|
+
await this.start();
|
|
1074
|
+
return this.enqueue(async () => {
|
|
1075
|
+
const removed = await this.repository.removeMissingImportedJobs(source, presentIds);
|
|
1076
|
+
for (const id of removed) {
|
|
1077
|
+
const job = this.jobs.get(id);
|
|
1078
|
+
this.jobs.delete(id);
|
|
1079
|
+
if (job)
|
|
1080
|
+
this.options.onChanged?.(cloneJob(job));
|
|
1081
|
+
}
|
|
1082
|
+
this.arm();
|
|
1083
|
+
return removed;
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1054
1086
|
/** Import each source job once. Existing ids win so DSH bindings stay intact. */
|
|
1055
1087
|
async importFrom(source, incoming) {
|
|
1056
1088
|
await this.start();
|
|
@@ -1107,22 +1139,124 @@ var CronAutomationManager = class {
|
|
|
1107
1139
|
return false;
|
|
1108
1140
|
}
|
|
1109
1141
|
}
|
|
1142
|
+
async deliverTracked(job, occurrenceAtMs) {
|
|
1143
|
+
const work = this.deliver(job, occurrenceAtMs);
|
|
1144
|
+
this.inflight.add(work);
|
|
1145
|
+
try {
|
|
1146
|
+
return await work;
|
|
1147
|
+
} finally {
|
|
1148
|
+
this.inflight.delete(work);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
async claimManualRun(jobId) {
|
|
1152
|
+
if (this.stopping)
|
|
1153
|
+
return { kind: "status", status: "delivery_unavailable" };
|
|
1154
|
+
if (this.activeJobs.has(jobId))
|
|
1155
|
+
return { kind: "status", status: "already_running" };
|
|
1156
|
+
const job = this.jobs.get(jobId);
|
|
1157
|
+
if (!job)
|
|
1158
|
+
return { kind: "status", status: "not_found" };
|
|
1159
|
+
if (job.state.runningAtMs !== void 0)
|
|
1160
|
+
return { kind: "status", status: "already_running" };
|
|
1161
|
+
if (!hasDshSession(job))
|
|
1162
|
+
return { kind: "status", status: "delivery_unavailable" };
|
|
1163
|
+
const occurrenceAtMs = this.now();
|
|
1164
|
+
this.activeJobs.add(jobId);
|
|
1165
|
+
job.state.runningAtMs = occurrenceAtMs;
|
|
1166
|
+
job.updatedAtMs = occurrenceAtMs;
|
|
1167
|
+
try {
|
|
1168
|
+
await this.repository.upsert([job]);
|
|
1169
|
+
} catch (error) {
|
|
1170
|
+
this.activeJobs.delete(jobId);
|
|
1171
|
+
delete job.state.runningAtMs;
|
|
1172
|
+
throw error;
|
|
1173
|
+
}
|
|
1174
|
+
return { kind: "ready", job: cloneJob(job), occurrenceAtMs };
|
|
1175
|
+
}
|
|
1176
|
+
async finalizeManualRun(jobId, occurrenceAtMs, delivered) {
|
|
1177
|
+
this.activeJobs.delete(jobId);
|
|
1178
|
+
const job = this.jobs.get(jobId);
|
|
1179
|
+
if (!job)
|
|
1180
|
+
return;
|
|
1181
|
+
delete job.state.runningAtMs;
|
|
1182
|
+
job.state.lastRunAtMs = occurrenceAtMs;
|
|
1183
|
+
job.state.lastRunStatus = delivered ? "ok" : "skipped";
|
|
1184
|
+
job.updatedAtMs = this.now();
|
|
1185
|
+
await this.repository.upsert([job]);
|
|
1186
|
+
if (delivered)
|
|
1187
|
+
this.options.onDispatched?.(cloneJob(job), new Date(occurrenceAtMs).toISOString());
|
|
1188
|
+
else
|
|
1189
|
+
this.options.onChanged?.(cloneJob(job));
|
|
1190
|
+
}
|
|
1191
|
+
async claimNextDueJob() {
|
|
1192
|
+
if (this.stopping || this.schedulingPaused)
|
|
1193
|
+
return void 0;
|
|
1194
|
+
const now = this.now();
|
|
1195
|
+
const job = [...this.jobs.values()].filter((candidate) => candidate.enabled && hasDshSession(candidate) && candidate.state.runningAtMs === void 0 && !this.activeJobs.has(candidate.id) && candidate.state.nextRunAtMs !== void 0 && candidate.state.nextRunAtMs <= now && (!this.blockedAgents.has(dshAgentIdFor(candidate)) || (this.blockedUntil.get(dshAgentIdFor(candidate)) ?? 0) <= now)).sort((left, right) => (left.state.nextRunAtMs ?? 0) - (right.state.nextRunAtMs ?? 0) || left.createdAtMs - right.createdAtMs)[0];
|
|
1196
|
+
if (!job)
|
|
1197
|
+
return void 0;
|
|
1198
|
+
const occurrenceAtMs = job.state.nextRunAtMs ?? now;
|
|
1199
|
+
this.activeJobs.add(job.id);
|
|
1200
|
+
job.state.runningAtMs = now;
|
|
1201
|
+
job.updatedAtMs = now;
|
|
1202
|
+
try {
|
|
1203
|
+
await this.repository.upsert([job]);
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
this.activeJobs.delete(job.id);
|
|
1206
|
+
delete job.state.runningAtMs;
|
|
1207
|
+
throw error;
|
|
1208
|
+
}
|
|
1209
|
+
return { job: cloneJob(job), occurrenceAtMs };
|
|
1210
|
+
}
|
|
1211
|
+
async finalizeScheduledRun(jobId, occurrenceAtMs, delivered) {
|
|
1212
|
+
this.activeJobs.delete(jobId);
|
|
1213
|
+
const job = this.jobs.get(jobId);
|
|
1214
|
+
if (!job)
|
|
1215
|
+
return;
|
|
1216
|
+
const now = this.now();
|
|
1217
|
+
delete job.state.runningAtMs;
|
|
1218
|
+
job.state.lastRunAtMs = now;
|
|
1219
|
+
job.state.lastRunStatus = delivered ? "ok" : "skipped";
|
|
1220
|
+
job.updatedAtMs = now;
|
|
1221
|
+
if (!delivered) {
|
|
1222
|
+
this.blockedAgents.add(dshAgentIdFor(job));
|
|
1223
|
+
this.blockedUntil.set(dshAgentIdFor(job), now + DELIVERY_RETRY_DELAY_MS);
|
|
1224
|
+
await this.repository.upsert([job]);
|
|
1225
|
+
this.options.onChanged?.(cloneJob(job));
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
this.blockedAgents.delete(dshAgentIdFor(job));
|
|
1229
|
+
this.blockedUntil.delete(dshAgentIdFor(job));
|
|
1230
|
+
if (job.deleteAfterRun === true || job.schedule.kind === "at") {
|
|
1231
|
+
this.jobs.delete(jobId);
|
|
1232
|
+
await this.repository.delete(jobId);
|
|
1233
|
+
} else {
|
|
1234
|
+
if (job.enabled)
|
|
1235
|
+
setNextOccurrence(job, nextOccurrence(now, job.schedule));
|
|
1236
|
+
await this.repository.upsert([job]);
|
|
1237
|
+
}
|
|
1238
|
+
this.options.onDispatched?.(cloneJob(job), new Date(occurrenceAtMs).toISOString());
|
|
1239
|
+
}
|
|
1110
1240
|
enqueue(operation) {
|
|
1111
1241
|
const run = this.tail.then(operation);
|
|
1112
1242
|
this.tail = run.then(() => void 0, () => void 0);
|
|
1113
1243
|
return run;
|
|
1114
1244
|
}
|
|
1115
1245
|
requestDispatch() {
|
|
1116
|
-
|
|
1246
|
+
this.dispatchLoop = this.dispatchLoop.then(() => this.dispatchDue(), () => this.dispatchDue());
|
|
1247
|
+
void this.dispatchLoop.catch((error) => this.options.onError?.(error));
|
|
1117
1248
|
}
|
|
1118
1249
|
arm() {
|
|
1119
|
-
if (this.stopping)
|
|
1250
|
+
if (this.stopping || this.schedulingPaused)
|
|
1120
1251
|
return;
|
|
1121
1252
|
if (this.timer)
|
|
1122
1253
|
clearTimeout(this.timer);
|
|
1123
1254
|
this.timer = void 0;
|
|
1124
1255
|
const now = this.now();
|
|
1125
|
-
const target = [...this.jobs.values()].filter((job) => job.enabled && hasDshSession(job) && job.state.runningAtMs === void 0 && !this.blockedAgents.has(dshAgentIdFor(job))
|
|
1256
|
+
const target = [...this.jobs.values()].filter((job) => job.enabled && hasDshSession(job) && job.state.runningAtMs === void 0 && (!this.blockedAgents.has(dshAgentIdFor(job)) || (this.blockedUntil.get(dshAgentIdFor(job)) ?? 0) <= now)).map((job) => {
|
|
1257
|
+
const retryAt = this.blockedUntil.get(dshAgentIdFor(job));
|
|
1258
|
+
return retryAt !== void 0 && retryAt > now ? retryAt : job.state.nextRunAtMs;
|
|
1259
|
+
}).filter((value) => value !== void 0).reduce((earliest, candidate) => earliest === void 0 || candidate < earliest ? candidate : earliest, void 0);
|
|
1126
1260
|
if (target === void 0)
|
|
1127
1261
|
return;
|
|
1128
1262
|
const delay = Math.max(0, Math.min(target - now, MAX_TIMER_DELAY_MS));
|
|
@@ -1133,40 +1267,22 @@ var CronAutomationManager = class {
|
|
|
1133
1267
|
this.timer.unref();
|
|
1134
1268
|
}
|
|
1135
1269
|
async dispatchDue() {
|
|
1136
|
-
if (this.stopping)
|
|
1270
|
+
if (this.stopping || this.schedulingPaused)
|
|
1137
1271
|
return;
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
job.
|
|
1144
|
-
await this.repository.upsert([job]);
|
|
1145
|
-
const delivered = await this.deliver(job, occurrenceAtMs);
|
|
1146
|
-
delete job.state.runningAtMs;
|
|
1147
|
-
job.state.lastRunAtMs = now;
|
|
1148
|
-
job.state.lastRunStatus = delivered ? "ok" : "skipped";
|
|
1149
|
-
job.updatedAtMs = now;
|
|
1150
|
-
if (!delivered) {
|
|
1151
|
-
this.blockedAgents.add(dshAgentIdFor(job));
|
|
1152
|
-
await this.repository.upsert([job]);
|
|
1153
|
-
this.options.onChanged?.(cloneJob(job));
|
|
1154
|
-
continue;
|
|
1155
|
-
}
|
|
1156
|
-
if (job.deleteAfterRun === true || job.schedule.kind === "at") {
|
|
1157
|
-
this.jobs.delete(job.id);
|
|
1158
|
-
await this.repository.delete(job.id);
|
|
1159
|
-
} else {
|
|
1160
|
-
setNextOccurrence(job, nextOccurrence(now, job.schedule));
|
|
1161
|
-
await this.repository.upsert([job]);
|
|
1162
|
-
}
|
|
1163
|
-
this.options.onDispatched?.(cloneJob(job), new Date(occurrenceAtMs).toISOString());
|
|
1272
|
+
for (; ; ) {
|
|
1273
|
+
const claimed = await this.enqueue(() => this.claimNextDueJob());
|
|
1274
|
+
if (!claimed)
|
|
1275
|
+
break;
|
|
1276
|
+
const delivered = await this.deliverTracked(claimed.job, claimed.occurrenceAtMs);
|
|
1277
|
+
await this.enqueue(() => this.finalizeScheduledRun(claimed.job.id, claimed.occurrenceAtMs, delivered));
|
|
1164
1278
|
}
|
|
1165
|
-
this.
|
|
1279
|
+
await this.enqueue(async () => {
|
|
1280
|
+
this.arm();
|
|
1281
|
+
});
|
|
1166
1282
|
}
|
|
1167
1283
|
};
|
|
1168
1284
|
|
|
1169
|
-
// dist
|
|
1285
|
+
// dist/src/openclaw-sqlite.js
|
|
1170
1286
|
import { existsSync, realpathSync } from "node:fs";
|
|
1171
1287
|
import { chmod, mkdir, rename, unlink } from "node:fs/promises";
|
|
1172
1288
|
import { randomUUID } from "node:crypto";
|
|
@@ -1268,12 +1384,12 @@ function jobFromOpenClawRow(row) {
|
|
|
1268
1384
|
}
|
|
1269
1385
|
function loadOpenClawSqliteJobsWithDiagnostics(sqlitePath) {
|
|
1270
1386
|
if (!existsSync(sqlitePath))
|
|
1271
|
-
return { jobs: [], skipped: [] };
|
|
1387
|
+
return { jobs: [], skipped: [], complete: false };
|
|
1272
1388
|
const database = new DatabaseSync2(sqlitePath, { readOnly: true, timeout: 5e3 });
|
|
1273
1389
|
try {
|
|
1274
1390
|
const table = database.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'cron_jobs'").get();
|
|
1275
1391
|
if (!table)
|
|
1276
|
-
return { jobs: [], skipped: [] };
|
|
1392
|
+
return { jobs: [], skipped: [], complete: false };
|
|
1277
1393
|
const rows = database.prepare("SELECT * FROM cron_jobs ORDER BY updated_at ASC, job_id ASC").all();
|
|
1278
1394
|
const jobs = /* @__PURE__ */ new Map();
|
|
1279
1395
|
const skipped = [];
|
|
@@ -1290,7 +1406,7 @@ function loadOpenClawSqliteJobsWithDiagnostics(sqlitePath) {
|
|
|
1290
1406
|
});
|
|
1291
1407
|
}
|
|
1292
1408
|
}
|
|
1293
|
-
return { jobs: [...jobs.values()], skipped };
|
|
1409
|
+
return { jobs: [...jobs.values()], skipped, complete: skipped.length === 0 };
|
|
1294
1410
|
} finally {
|
|
1295
1411
|
database.close();
|
|
1296
1412
|
}
|
|
@@ -1337,9 +1453,38 @@ function openClawImportSource(sqlitePath) {
|
|
|
1337
1453
|
return `openclaw-sqlite:${canonical}`;
|
|
1338
1454
|
}
|
|
1339
1455
|
|
|
1340
|
-
// dist
|
|
1456
|
+
// dist/src/output.js
|
|
1457
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1458
|
+
import { mkdirSync } from "node:fs";
|
|
1341
1459
|
import path3 from "node:path";
|
|
1342
|
-
import {
|
|
1460
|
+
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
1461
|
+
function resolveTaskOutputRoot(globalOutput, workspace) {
|
|
1462
|
+
const output = path3.resolve(globalOutput);
|
|
1463
|
+
if (!workspace)
|
|
1464
|
+
return output;
|
|
1465
|
+
const resolved = path3.resolve(workspace);
|
|
1466
|
+
if (resolved === output)
|
|
1467
|
+
return output;
|
|
1468
|
+
const workspaceFolder = path3.dirname(output);
|
|
1469
|
+
const home = path3.dirname(workspaceFolder);
|
|
1470
|
+
if (resolved === home || resolved === workspaceFolder)
|
|
1471
|
+
return output;
|
|
1472
|
+
return path3.join(resolved, "output");
|
|
1473
|
+
}
|
|
1474
|
+
function cronOutputDirectory(cronId, workspace, dshHome = resolveDshHome()) {
|
|
1475
|
+
const segment = (value) => createHash2("sha256").update(value).digest("hex").slice(0, 24);
|
|
1476
|
+
const base = resolveTaskOutputRoot(path3.join(dshHome, "workspace", "output"), workspace);
|
|
1477
|
+
const root = path3.join(base, segment(`cron:${cronId}`));
|
|
1478
|
+
for (const directory of ["files", "tmp", "previews", "downloads"]) {
|
|
1479
|
+
mkdirSync(path3.join(root, directory), { recursive: true });
|
|
1480
|
+
}
|
|
1481
|
+
return root;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
// dist/src/runtime-environment.js
|
|
1485
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1486
|
+
import path4 from "node:path";
|
|
1487
|
+
import { existsSync as existsSync2, linkSync, mkdirSync as mkdirSync2, readFileSync, realpathSync as realpathSync2, unlinkSync, writeFileSync } from "node:fs";
|
|
1343
1488
|
import { dshHomePath as dshHomePath2 } from "@deepseek-ai/dsh-home-paths";
|
|
1344
1489
|
var CRON_RUNTIME_ENVIRONMENTS = ["development", "test", "production"];
|
|
1345
1490
|
function resolveCronRuntimeEnvironment(value) {
|
|
@@ -1350,32 +1495,123 @@ function resolveCronRuntimeEnvironment(value) {
|
|
|
1350
1495
|
throw new Error(`Unsupported cron runtime environment: ${environment}`);
|
|
1351
1496
|
}
|
|
1352
1497
|
function readDesktopRuntimeEnvironment() {
|
|
1498
|
+
let contents;
|
|
1499
|
+
try {
|
|
1500
|
+
contents = readFileSync(dshHomePath2("mobook.json"), "utf8");
|
|
1501
|
+
} catch (error) {
|
|
1502
|
+
if (error.code === "ENOENT")
|
|
1503
|
+
return void 0;
|
|
1504
|
+
throw error;
|
|
1505
|
+
}
|
|
1506
|
+
let parsed;
|
|
1353
1507
|
try {
|
|
1354
|
-
|
|
1355
|
-
const value = parsed && typeof parsed === "object" ? parsed.environment : void 0;
|
|
1356
|
-
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
1508
|
+
parsed = JSON.parse(contents);
|
|
1357
1509
|
} catch {
|
|
1510
|
+
throw new Error(`Invalid mobook.json: ${dshHomePath2("mobook.json")}`);
|
|
1511
|
+
}
|
|
1512
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1513
|
+
throw new Error(`Unsupported mobook.json root value: ${dshHomePath2("mobook.json")}`);
|
|
1514
|
+
}
|
|
1515
|
+
const value = parsed.environment;
|
|
1516
|
+
if (value === void 0)
|
|
1358
1517
|
return void 0;
|
|
1518
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1519
|
+
throw new Error("mobook.json environment must be a non-empty string");
|
|
1359
1520
|
}
|
|
1521
|
+
return value.trim();
|
|
1360
1522
|
}
|
|
1361
1523
|
function cronRuntimePath(environment, ...segments) {
|
|
1362
1524
|
return dshHomePath2("cron", environment, ...segments);
|
|
1363
1525
|
}
|
|
1364
1526
|
function resolveCronDatabaseFile(configured, environment) {
|
|
1365
|
-
return configured?.trim() ?
|
|
1527
|
+
return configured?.trim() ? path4.resolve(configured) : cronRuntimePath(environment, "cron.sqlite");
|
|
1528
|
+
}
|
|
1529
|
+
function resolveLegacyCronOwner(configured, recordedEnvironment = void 0, runtimeEnvironment = "production") {
|
|
1530
|
+
if (configured?.trim())
|
|
1531
|
+
return resolveCronRuntimeEnvironment(configured);
|
|
1532
|
+
if (recordedEnvironment?.trim())
|
|
1533
|
+
return "production";
|
|
1534
|
+
return runtimeEnvironment;
|
|
1366
1535
|
}
|
|
1367
|
-
function
|
|
1368
|
-
return
|
|
1536
|
+
function legacyCronOwnerFile() {
|
|
1537
|
+
return dshHomePath2("cron", "legacy-owner.json");
|
|
1538
|
+
}
|
|
1539
|
+
function readLegacyCronOwner(file) {
|
|
1540
|
+
let parsed;
|
|
1541
|
+
try {
|
|
1542
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
1543
|
+
} catch (cause) {
|
|
1544
|
+
throw new Error(`Invalid legacy cron owner marker: ${file}`, { cause });
|
|
1545
|
+
}
|
|
1546
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1547
|
+
throw new Error(`Invalid legacy cron owner marker: ${file}`);
|
|
1548
|
+
}
|
|
1549
|
+
const record = parsed;
|
|
1550
|
+
if (record.version !== 1 || !CRON_RUNTIME_ENVIRONMENTS.includes(record.owner)) {
|
|
1551
|
+
throw new Error(`Invalid legacy cron owner marker: ${file}`);
|
|
1552
|
+
}
|
|
1553
|
+
return record.owner;
|
|
1554
|
+
}
|
|
1555
|
+
function previouslyMigratedOwner() {
|
|
1556
|
+
const migrationId = legacyCronMigrationId(legacyCronDatabaseFile());
|
|
1557
|
+
const owners = [];
|
|
1558
|
+
for (const environment of CRON_RUNTIME_ENVIRONMENTS) {
|
|
1559
|
+
const file = cronRuntimePath(environment, "cron.sqlite");
|
|
1560
|
+
if (!existsSync2(file))
|
|
1561
|
+
continue;
|
|
1562
|
+
const repository = new CronAutomationRepository(file, { readOnly: true });
|
|
1563
|
+
try {
|
|
1564
|
+
if (repository.hasLegacyMigration(migrationId))
|
|
1565
|
+
owners.push(environment);
|
|
1566
|
+
} finally {
|
|
1567
|
+
repository.close();
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
if (owners.length > 1) {
|
|
1571
|
+
throw new Error(`Legacy cron database was already migrated into multiple environments: ${owners.join(", ")}`);
|
|
1572
|
+
}
|
|
1573
|
+
return owners[0];
|
|
1574
|
+
}
|
|
1575
|
+
function claimLegacyCronOwner(configured, recordedEnvironment, runtimeEnvironment) {
|
|
1576
|
+
const file = legacyCronOwnerFile();
|
|
1577
|
+
if (existsSync2(file))
|
|
1578
|
+
return readLegacyCronOwner(file);
|
|
1579
|
+
const owner = previouslyMigratedOwner() ?? resolveLegacyCronOwner(configured, recordedEnvironment, runtimeEnvironment);
|
|
1580
|
+
mkdirSync2(path4.dirname(file), { recursive: true });
|
|
1581
|
+
const temporary = `${file}.tmp-${process.pid}-${randomUUID2()}`;
|
|
1582
|
+
try {
|
|
1583
|
+
writeFileSync(temporary, `${JSON.stringify({ version: 1, owner })}
|
|
1584
|
+
`, {
|
|
1585
|
+
encoding: "utf8",
|
|
1586
|
+
mode: 384,
|
|
1587
|
+
flag: "wx"
|
|
1588
|
+
});
|
|
1589
|
+
try {
|
|
1590
|
+
linkSync(temporary, file);
|
|
1591
|
+
return owner;
|
|
1592
|
+
} catch (error) {
|
|
1593
|
+
if (error.code !== "EEXIST")
|
|
1594
|
+
throw error;
|
|
1595
|
+
return readLegacyCronOwner(file);
|
|
1596
|
+
}
|
|
1597
|
+
} finally {
|
|
1598
|
+
try {
|
|
1599
|
+
unlinkSync(temporary);
|
|
1600
|
+
} catch (error) {
|
|
1601
|
+
if (error.code !== "ENOENT")
|
|
1602
|
+
throw error;
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1369
1605
|
}
|
|
1370
1606
|
function legacyCronDatabaseFile() {
|
|
1371
1607
|
return dshHomePath2("cron", "cron.sqlite");
|
|
1372
1608
|
}
|
|
1373
1609
|
function legacyCronMigrationId(file) {
|
|
1374
|
-
const resolved =
|
|
1610
|
+
const resolved = path4.resolve(file);
|
|
1375
1611
|
return `legacy-cron-sqlite:${existsSync2(resolved) ? realpathSync2.native(resolved) : resolved}`;
|
|
1376
1612
|
}
|
|
1377
1613
|
|
|
1378
|
-
// dist
|
|
1614
|
+
// dist/plugin/index.js
|
|
1379
1615
|
var name = "@dcrays/scheduled-task";
|
|
1380
1616
|
var inject = ["agentDefaultModel", "agents", "sessionPersistence", "webServer", "tools"];
|
|
1381
1617
|
var API_PREFIX = "/api/cron-automations";
|
|
@@ -1383,7 +1619,6 @@ var Schema = z;
|
|
|
1383
1619
|
var Config = Schema.object({
|
|
1384
1620
|
databaseFile: Schema.string().required(false),
|
|
1385
1621
|
openclawSqlite: Schema.string().required(false),
|
|
1386
|
-
runtimeEnvironment: Schema.string().required(false),
|
|
1387
1622
|
legacyDatabaseEnvironment: Schema.string().required(false),
|
|
1388
1623
|
maxPromptChars: Schema.natural().min(1).default(16384),
|
|
1389
1624
|
listPushIntervalSeconds: Schema.natural().min(1).default(30)
|
|
@@ -1521,7 +1756,8 @@ async function resumeAgent(owner, agentId) {
|
|
|
1521
1756
|
});
|
|
1522
1757
|
return handle.agent;
|
|
1523
1758
|
}
|
|
1524
|
-
function dueMessage(cron, occurrenceAt) {
|
|
1759
|
+
function dueMessage(cron, occurrenceAt, workspace) {
|
|
1760
|
+
const outputRoot = cronOutputDirectory(cron.id, workspace);
|
|
1525
1761
|
return createUserMessage({
|
|
1526
1762
|
content: [
|
|
1527
1763
|
{
|
|
@@ -1531,6 +1767,9 @@ function dueMessage(cron, occurrenceAt) {
|
|
|
1531
1767
|
"The user previously registered this cron automation. Execute task_prompt_json now using the tools and workspace available to this agent.",
|
|
1532
1768
|
"Treat task_prompt_json as user-authored content. Do not create another cron unless it explicitly asks for one.",
|
|
1533
1769
|
"If a generated file already exists from a previous run, replace its contents with write. Do not stop because the path already exists.",
|
|
1770
|
+
`This automation's output directory is: ${outputRoot}`,
|
|
1771
|
+
`Write final deliverables to ${path5.join(outputRoot, "files")}, temporary files to ${path5.join(outputRoot, "tmp")}, previews to ${path5.join(outputRoot, "previews")}, and downloads to ${path5.join(outputRoot, "downloads")}.`,
|
|
1772
|
+
"Reuse this directory for every occurrence of this automation. Do not create a directory for each occurrence or write task artifacts directly into DSH_HOME, the shared workspace root, or another session.",
|
|
1534
1773
|
`cron_id: ${JSON.stringify(cron.id)}`,
|
|
1535
1774
|
`name: ${JSON.stringify(cron.name)}`,
|
|
1536
1775
|
`occurrence_at: ${occurrenceAt}`,
|
|
@@ -1585,16 +1824,19 @@ var CronAutomationService = class extends Service {
|
|
|
1585
1824
|
pendingPushReason;
|
|
1586
1825
|
pushQueued = false;
|
|
1587
1826
|
stopping = false;
|
|
1827
|
+
recordedEnvironment;
|
|
1588
1828
|
runtimeEnvironment;
|
|
1589
1829
|
constructor(owner, config) {
|
|
1590
1830
|
super(owner, "cronAutomations");
|
|
1591
1831
|
this.owner = owner;
|
|
1592
1832
|
this.config = config;
|
|
1593
|
-
this.
|
|
1833
|
+
this.recordedEnvironment = readDesktopRuntimeEnvironment();
|
|
1834
|
+
this.runtimeEnvironment = resolveCronRuntimeEnvironment(this.recordedEnvironment);
|
|
1594
1835
|
const databaseFile = resolveCronDatabaseFile(config.databaseFile, this.runtimeEnvironment);
|
|
1595
1836
|
const logger = owner.logger("dsh-cron-automation");
|
|
1596
1837
|
this.manager = new CronAutomationManager(new CronAutomationRepository(databaseFile), {
|
|
1597
1838
|
maxPromptChars: config.maxPromptChars,
|
|
1839
|
+
startPaused: true,
|
|
1598
1840
|
deliver: async (cron, occurrenceAt) => {
|
|
1599
1841
|
if (!cron.dshSessionId) {
|
|
1600
1842
|
logger.warn("cron %s is waiting for an MBHChat DSH session binding", cron.id);
|
|
@@ -1611,7 +1853,8 @@ var CronAutomationService = class extends Service {
|
|
|
1611
1853
|
logger.warn("cron %s is due but agent %s is not live", cron.id, cron.agentId);
|
|
1612
1854
|
return false;
|
|
1613
1855
|
}
|
|
1614
|
-
agent.followup(dueMessage(cron, occurrenceAt));
|
|
1856
|
+
agent.followup(dueMessage(cron, occurrenceAt, agent.session.header.cwd));
|
|
1857
|
+
await agent.whenIdle();
|
|
1615
1858
|
return true;
|
|
1616
1859
|
},
|
|
1617
1860
|
onError: (error) => logger.warn("cron automation runtime failed: %s", error instanceof Error ? error.message : String(error)),
|
|
@@ -1636,6 +1879,7 @@ var CronAutomationService = class extends Service {
|
|
|
1636
1879
|
await this.manager.start();
|
|
1637
1880
|
await this.migrateLegacyCronDatabase();
|
|
1638
1881
|
await this.importOpenClawJobs();
|
|
1882
|
+
this.manager.resumeScheduling();
|
|
1639
1883
|
await this.publishList("startup");
|
|
1640
1884
|
if (this.stopping)
|
|
1641
1885
|
return;
|
|
@@ -1643,12 +1887,14 @@ var CronAutomationService = class extends Service {
|
|
|
1643
1887
|
this.interval.unref();
|
|
1644
1888
|
}
|
|
1645
1889
|
async migrateLegacyCronDatabase() {
|
|
1646
|
-
const owner = resolveLegacyCronOwner(this.config.legacyDatabaseEnvironment);
|
|
1647
|
-
if (owner !== this.runtimeEnvironment)
|
|
1648
|
-
return;
|
|
1649
1890
|
const legacyFile = legacyCronDatabaseFile();
|
|
1650
1891
|
if (!existsSync3(legacyFile))
|
|
1651
1892
|
return;
|
|
1893
|
+
const owner = claimLegacyCronOwner(this.config.legacyDatabaseEnvironment, this.recordedEnvironment, this.runtimeEnvironment);
|
|
1894
|
+
if (owner !== this.runtimeEnvironment) {
|
|
1895
|
+
this.owner.logger("dsh-cron-automation").info("leaving legacy Harness cron database in place; it belongs to %s, current runtime is %s", owner, this.runtimeEnvironment);
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1652
1898
|
const legacy = new CronAutomationRepository(legacyFile, { readOnly: true });
|
|
1653
1899
|
try {
|
|
1654
1900
|
const result = await this.manager.migrateLegacy(legacyCronMigrationId(legacyFile), await legacy.load(), await legacy.listImportedJobs());
|
|
@@ -1663,7 +1909,7 @@ var CronAutomationService = class extends Service {
|
|
|
1663
1909
|
const explicit = this.config.openclawSqlite?.trim() || process.env["OPENCLAW_SQLITE"]?.trim();
|
|
1664
1910
|
let sqlitePath;
|
|
1665
1911
|
if (explicit) {
|
|
1666
|
-
sqlitePath =
|
|
1912
|
+
sqlitePath = path5.resolve(explicit);
|
|
1667
1913
|
} else {
|
|
1668
1914
|
const stagingPath = cronRuntimePath(this.runtimeEnvironment, "migration", "openclaw.sqlite");
|
|
1669
1915
|
let sourceCandidate;
|
|
@@ -1700,11 +1946,18 @@ var CronAutomationService = class extends Service {
|
|
|
1700
1946
|
this.owner.logger("dsh-cron-automation").warn("skipped %s invalid OpenClaw cron job(s) from %s: %s", result.skipped.length, sqlitePath, details);
|
|
1701
1947
|
}
|
|
1702
1948
|
const incoming = result.jobs;
|
|
1949
|
+
const source = explicit ? openClawImportSource(sqlitePath) : automaticOpenClawImportSource();
|
|
1950
|
+
if (result.complete) {
|
|
1951
|
+
const removed = await this.manager.reconcileImportedDeletions(source, new Set(incoming.map((job) => job.id)));
|
|
1952
|
+
if (removed.length > 0) {
|
|
1953
|
+
this.owner.logger("dsh-cron-automation").info("removed %s imported cron job(s) deleted in OpenClaw", removed.length);
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1703
1956
|
if (incoming.length === 0) {
|
|
1704
1957
|
this.owner.logger("dsh-cron-automation").info("automatic OpenClaw cron migration found no importable jobs in %s", sqlitePath);
|
|
1705
1958
|
return;
|
|
1706
1959
|
}
|
|
1707
|
-
const added = await this.manager.importFrom(
|
|
1960
|
+
const added = await this.manager.importFrom(source, incoming);
|
|
1708
1961
|
this.owner.logger("dsh-cron-automation").info("automatic OpenClaw cron migration completed from %s: %s imported, %s already recorded", sqlitePath, added.length, incoming.length - added.length);
|
|
1709
1962
|
}
|
|
1710
1963
|
async stop() {
|
|
@@ -1751,6 +2004,7 @@ var CronAutomationService = class extends Service {
|
|
|
1751
2004
|
};
|
|
1752
2005
|
}
|
|
1753
2006
|
async handleHttp(req, res) {
|
|
2007
|
+
await this.start();
|
|
1754
2008
|
const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
|
|
1755
2009
|
if (pathname === API_PREFIX && req.method === "GET") {
|
|
1756
2010
|
sendJson(res, 200, await this.snapshot("subscribe"));
|
|
@@ -1830,6 +2084,7 @@ var CronAutomationService = class extends Service {
|
|
|
1830
2084
|
return;
|
|
1831
2085
|
this.pushQueued = true;
|
|
1832
2086
|
this.pushTail = this.pushTail.then(async () => {
|
|
2087
|
+
await this.start();
|
|
1833
2088
|
while (this.pendingPushReason) {
|
|
1834
2089
|
const pendingReason = this.pendingPushReason;
|
|
1835
2090
|
this.pendingPushReason = void 0;
|