@dcrays/scheduled-task 0.1.1 → 0.1.3
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 +2 -0
- package/package.json +1 -1
- package/plugin/index.d.ts +4 -0
- package/plugin/index.js +178 -30
- package/src/manager.d.ts +6 -1
- package/src/manager.js +23 -0
- package/src/openclaw-sqlite.d.ts +10 -3
- package/src/openclaw-sqlite.js +50 -19
- package/src/repository.d.ts +9 -0
- package/src/repository.js +37 -0
- package/src/runtime-environment.d.ts +13 -0
- package/src/runtime-environment.js +32 -0
package/cordis.patch.yml
CHANGED
package/package.json
CHANGED
package/plugin/index.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export declare const inject: string[];
|
|
|
9
9
|
export interface Config {
|
|
10
10
|
databaseFile?: string;
|
|
11
11
|
openclawSqlite?: string;
|
|
12
|
+
runtimeEnvironment?: string;
|
|
13
|
+
legacyDatabaseEnvironment?: string;
|
|
12
14
|
maxPromptChars: number;
|
|
13
15
|
listPushIntervalSeconds: number;
|
|
14
16
|
}
|
|
@@ -45,9 +47,11 @@ export declare class CronAutomationService extends Service {
|
|
|
45
47
|
private pendingPushReason;
|
|
46
48
|
private pushQueued;
|
|
47
49
|
private stopping;
|
|
50
|
+
private readonly runtimeEnvironment;
|
|
48
51
|
constructor(owner: Context, config: Config);
|
|
49
52
|
start(): Promise<void>;
|
|
50
53
|
private initialize;
|
|
54
|
+
private migrateLegacyCronDatabase;
|
|
51
55
|
private importOpenClawJobs;
|
|
52
56
|
stop(): Promise<void>;
|
|
53
57
|
create(agentId: string, envelope: CronCreateEnvelope): Promise<StoredCronAutomation>;
|
package/plugin/index.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { createRequire as __mobookCreateRequire } from 'node:module'; const require = __mobookCreateRequire(import.meta.url);
|
|
2
2
|
|
|
3
3
|
// dist-npm/plugin/index.js
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import
|
|
4
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
5
|
+
import path4 from "node:path";
|
|
6
6
|
import { Service } from "@deepseek-ai/cordis";
|
|
7
|
-
import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
|
|
8
7
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
9
8
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
10
9
|
import z from "@deepseek-ai/schemastery";
|
|
@@ -516,6 +515,11 @@ var CronAutomationRepository = class {
|
|
|
516
515
|
imported_at INTEGER NOT NULL,
|
|
517
516
|
PRIMARY KEY (source, job_id)
|
|
518
517
|
);
|
|
518
|
+
|
|
519
|
+
CREATE TABLE IF NOT EXISTS cron_migrations (
|
|
520
|
+
migration_id TEXT PRIMARY KEY,
|
|
521
|
+
completed_at INTEGER NOT NULL
|
|
522
|
+
);
|
|
519
523
|
`);
|
|
520
524
|
this.insertJobStatement = this.database.prepare(`
|
|
521
525
|
INSERT INTO cron_jobs (
|
|
@@ -623,6 +627,34 @@ var CronAutomationRepository = class {
|
|
|
623
627
|
return result;
|
|
624
628
|
});
|
|
625
629
|
}
|
|
630
|
+
async listImportedJobs() {
|
|
631
|
+
this.assertOpen();
|
|
632
|
+
const table = this.database.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'cron_imported_jobs'").get();
|
|
633
|
+
if (!table)
|
|
634
|
+
return [];
|
|
635
|
+
return this.database.prepare("SELECT source, job_id FROM cron_imported_jobs").all().map((row) => ({ source: row.source, jobId: row.job_id }));
|
|
636
|
+
}
|
|
637
|
+
async migrateLegacy(migrationId, incoming, importedJobs) {
|
|
638
|
+
this.assertWritable();
|
|
639
|
+
const decoded = uniqueJobs(incoming);
|
|
640
|
+
return this.writeTransaction(() => {
|
|
641
|
+
const completed = this.database.prepare("SELECT 1 FROM cron_migrations WHERE migration_id = ?").get(migrationId);
|
|
642
|
+
if (completed)
|
|
643
|
+
return { added: [], skippedIds: [], alreadyMigrated: true };
|
|
644
|
+
const result = this.planImport(migrationId, decoded);
|
|
645
|
+
const nextOrder = this.nextSortOrder();
|
|
646
|
+
result.added.forEach((job, index) => this.insertJob(job, nextOrder + index));
|
|
647
|
+
const markImported = this.database.prepare(`
|
|
648
|
+
INSERT OR IGNORE INTO cron_imported_jobs (source, job_id, imported_at)
|
|
649
|
+
VALUES (?, ?, ?)
|
|
650
|
+
`);
|
|
651
|
+
const importedAt = Date.now();
|
|
652
|
+
for (const job of importedJobs)
|
|
653
|
+
markImported.run(job.source, job.jobId, importedAt);
|
|
654
|
+
this.database.prepare("INSERT INTO cron_migrations (migration_id, completed_at) VALUES (?, ?)").run(migrationId, importedAt);
|
|
655
|
+
return { ...result, alreadyMigrated: false };
|
|
656
|
+
});
|
|
657
|
+
}
|
|
626
658
|
close() {
|
|
627
659
|
if (this.closed)
|
|
628
660
|
return;
|
|
@@ -1044,6 +1076,29 @@ var CronAutomationManager = class {
|
|
|
1044
1076
|
return added.map(cloneJob);
|
|
1045
1077
|
});
|
|
1046
1078
|
}
|
|
1079
|
+
async migrateLegacy(migrationId, incoming, importedJobs) {
|
|
1080
|
+
await this.start();
|
|
1081
|
+
return this.enqueue(async () => {
|
|
1082
|
+
const prepared = incoming.map((job) => this.prepareImportedJob(job));
|
|
1083
|
+
const result = await this.repository.migrateLegacy(migrationId, prepared, importedJobs);
|
|
1084
|
+
if (result.alreadyMigrated || result.added.length === 0)
|
|
1085
|
+
return { added: [], alreadyMigrated: result.alreadyMigrated };
|
|
1086
|
+
for (const job of result.added) {
|
|
1087
|
+
this.jobs.set(job.id, cloneJob(job));
|
|
1088
|
+
this.options.onCreated?.(cloneJob(job));
|
|
1089
|
+
}
|
|
1090
|
+
this.arm();
|
|
1091
|
+
return { added: result.added.map(cloneJob), alreadyMigrated: false };
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
prepareImportedJob(job) {
|
|
1095
|
+
const next = cloneJob(job);
|
|
1096
|
+
delete next.state.runningAtMs;
|
|
1097
|
+
disableIfTooFrequent(next);
|
|
1098
|
+
if (next.enabled && next.state.nextRunAtMs === void 0)
|
|
1099
|
+
setNextOccurrence(next, initialOccurrence(next, this.now()));
|
|
1100
|
+
return next;
|
|
1101
|
+
}
|
|
1047
1102
|
async deliver(job, occurrenceAtMs) {
|
|
1048
1103
|
try {
|
|
1049
1104
|
return await this.options.deliver(cloneJob(job), new Date(occurrenceAtMs).toISOString());
|
|
@@ -1113,13 +1168,12 @@ var CronAutomationManager = class {
|
|
|
1113
1168
|
|
|
1114
1169
|
// dist-npm/src/openclaw-sqlite.js
|
|
1115
1170
|
import { existsSync, realpathSync } from "node:fs";
|
|
1116
|
-
import { mkdir, rename, unlink
|
|
1171
|
+
import { chmod, mkdir, rename, unlink } from "node:fs/promises";
|
|
1117
1172
|
import { randomUUID } from "node:crypto";
|
|
1118
1173
|
import os from "node:os";
|
|
1119
1174
|
import path2 from "node:path";
|
|
1120
|
-
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
1121
|
-
|
|
1122
|
-
var DEFAULT_OPENCLAW_SQLITE_ALT = path2.join(os.homedir(), ".openclaw", "state", "openclaw.sqlite");
|
|
1175
|
+
import { backup, DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
1176
|
+
import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
|
|
1123
1177
|
function isRecord3(value) {
|
|
1124
1178
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1125
1179
|
}
|
|
@@ -1181,18 +1235,22 @@ function overlayState(row, jobState, tableState) {
|
|
|
1181
1235
|
...isRecord3(jobState) ? jobState : {},
|
|
1182
1236
|
...isRecord3(tableState) ? tableState : {}
|
|
1183
1237
|
};
|
|
1184
|
-
if (row.next_run_at_ms
|
|
1238
|
+
if (typeof row.next_run_at_ms === "number")
|
|
1185
1239
|
state.nextRunAtMs = row.next_run_at_ms;
|
|
1186
|
-
if (row.running_at_ms
|
|
1240
|
+
if (typeof row.running_at_ms === "number")
|
|
1187
1241
|
state.runningAtMs = row.running_at_ms;
|
|
1188
|
-
if (row.last_run_at_ms
|
|
1242
|
+
if (typeof row.last_run_at_ms === "number")
|
|
1189
1243
|
state.lastRunAtMs = row.last_run_at_ms;
|
|
1190
1244
|
if (row.last_run_status)
|
|
1191
1245
|
state.lastRunStatus = row.last_run_status;
|
|
1192
1246
|
return state;
|
|
1193
1247
|
}
|
|
1194
1248
|
function jobFromOpenClawRow(row) {
|
|
1195
|
-
|
|
1249
|
+
let tableState = {};
|
|
1250
|
+
try {
|
|
1251
|
+
tableState = parseJson(row.state_json || "{}", "state_json");
|
|
1252
|
+
} catch {
|
|
1253
|
+
}
|
|
1196
1254
|
try {
|
|
1197
1255
|
const parsed = parseJson(row.job_json, "job_json");
|
|
1198
1256
|
if (!isRecord3(parsed))
|
|
@@ -1208,26 +1266,31 @@ function jobFromOpenClawRow(row) {
|
|
|
1208
1266
|
return decodeJob(jobFromColumns(row, overlayState(row, {}, tableState)));
|
|
1209
1267
|
}
|
|
1210
1268
|
}
|
|
1211
|
-
function
|
|
1269
|
+
function loadOpenClawSqliteJobsWithDiagnostics(sqlitePath) {
|
|
1212
1270
|
if (!existsSync(sqlitePath))
|
|
1213
|
-
return [];
|
|
1271
|
+
return { jobs: [], skipped: [] };
|
|
1214
1272
|
const database = new DatabaseSync2(sqlitePath, { readOnly: true, timeout: 5e3 });
|
|
1215
1273
|
try {
|
|
1216
1274
|
const table = database.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'cron_jobs'").get();
|
|
1217
1275
|
if (!table)
|
|
1218
|
-
return [];
|
|
1276
|
+
return { jobs: [], skipped: [] };
|
|
1219
1277
|
const rows = database.prepare("SELECT * FROM cron_jobs ORDER BY updated_at ASC, job_id ASC").all();
|
|
1220
1278
|
const jobs = /* @__PURE__ */ new Map();
|
|
1279
|
+
const skipped = [];
|
|
1221
1280
|
for (const row of rows) {
|
|
1222
1281
|
try {
|
|
1223
1282
|
const job = jobFromOpenClawRow(row);
|
|
1224
1283
|
const previous = jobs.get(job.id);
|
|
1225
1284
|
if (!previous || job.updatedAtMs >= previous.updatedAtMs)
|
|
1226
1285
|
jobs.set(job.id, job);
|
|
1227
|
-
} catch {
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
skipped.push({
|
|
1288
|
+
jobId: typeof row.job_id === "string" && row.job_id ? row.job_id : "<unknown>",
|
|
1289
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1290
|
+
});
|
|
1228
1291
|
}
|
|
1229
1292
|
}
|
|
1230
|
-
return [...jobs.values()];
|
|
1293
|
+
return { jobs: [...jobs.values()], skipped };
|
|
1231
1294
|
} finally {
|
|
1232
1295
|
database.close();
|
|
1233
1296
|
}
|
|
@@ -1235,12 +1298,16 @@ function loadOpenClawSqliteJobs(sqlitePath) {
|
|
|
1235
1298
|
async function snapshotOpenClawSqlite(sqlitePath, destination) {
|
|
1236
1299
|
const database = new DatabaseSync2(sqlitePath, { readOnly: true, timeout: 5e3 });
|
|
1237
1300
|
try {
|
|
1238
|
-
const serialized = database.serialize();
|
|
1239
1301
|
const directory = path2.dirname(destination);
|
|
1240
1302
|
const temporary = path2.join(directory, `.${path2.basename(destination)}.${randomUUID()}.tmp`);
|
|
1241
1303
|
await mkdir(directory, { recursive: true });
|
|
1242
1304
|
try {
|
|
1243
|
-
await
|
|
1305
|
+
await backup(database, temporary);
|
|
1306
|
+
await chmod(temporary, 384);
|
|
1307
|
+
await Promise.all([
|
|
1308
|
+
unlink(`${destination}-wal`).catch(() => void 0),
|
|
1309
|
+
unlink(`${destination}-shm`).catch(() => void 0)
|
|
1310
|
+
]);
|
|
1244
1311
|
await rename(temporary, destination);
|
|
1245
1312
|
} catch (error) {
|
|
1246
1313
|
await unlink(temporary).catch(() => void 0);
|
|
@@ -1250,12 +1317,55 @@ async function snapshotOpenClawSqlite(sqlitePath, destination) {
|
|
|
1250
1317
|
database.close();
|
|
1251
1318
|
}
|
|
1252
1319
|
}
|
|
1320
|
+
function resolveOpenClawSqlitePath(configured, targetHome = dshHomePath("."), userHome = os.homedir()) {
|
|
1321
|
+
const explicit = configured?.trim() || process.env.OPENCLAW_SQLITE?.trim();
|
|
1322
|
+
if (explicit)
|
|
1323
|
+
return path2.resolve(explicit);
|
|
1324
|
+
const target = path2.basename(path2.resolve(targetHome));
|
|
1325
|
+
if (target === ".mobook-harness")
|
|
1326
|
+
return path2.join(userHome, ".mobook", "state", "openclaw.sqlite");
|
|
1327
|
+
if (target === ".dsh")
|
|
1328
|
+
return path2.join(userHome, ".openclaw", "state", "openclaw.sqlite");
|
|
1329
|
+
throw new Error("Custom DSH home requires an explicit openclawSqlite or OPENCLAW_SQLITE migration source");
|
|
1330
|
+
}
|
|
1331
|
+
function automaticOpenClawImportSource(targetHome = dshHomePath(".")) {
|
|
1332
|
+
return openClawImportSource(path2.join(targetHome, "migration", "openclaw.sqlite"));
|
|
1333
|
+
}
|
|
1253
1334
|
function openClawImportSource(sqlitePath) {
|
|
1254
1335
|
const resolved = path2.resolve(sqlitePath);
|
|
1255
1336
|
const canonical = existsSync(resolved) ? realpathSync.native(resolved) : resolved;
|
|
1256
1337
|
return `openclaw-sqlite:${canonical}`;
|
|
1257
1338
|
}
|
|
1258
1339
|
|
|
1340
|
+
// dist-npm/src/runtime-environment.js
|
|
1341
|
+
import path3 from "node:path";
|
|
1342
|
+
import { existsSync as existsSync2, realpathSync as realpathSync2 } from "node:fs";
|
|
1343
|
+
import { dshHomePath as dshHomePath2 } from "@deepseek-ai/dsh-home-paths";
|
|
1344
|
+
var CRON_RUNTIME_ENVIRONMENTS = ["development", "test", "production"];
|
|
1345
|
+
function resolveCronRuntimeEnvironment(value) {
|
|
1346
|
+
const environment = value?.trim() || "development";
|
|
1347
|
+
if (CRON_RUNTIME_ENVIRONMENTS.includes(environment)) {
|
|
1348
|
+
return environment;
|
|
1349
|
+
}
|
|
1350
|
+
throw new Error(`Unsupported cron runtime environment: ${environment}`);
|
|
1351
|
+
}
|
|
1352
|
+
function cronRuntimePath(environment, ...segments) {
|
|
1353
|
+
return dshHomePath2("cron", environment, ...segments);
|
|
1354
|
+
}
|
|
1355
|
+
function resolveCronDatabaseFile(configured, environment) {
|
|
1356
|
+
return configured?.trim() ? path3.resolve(configured) : cronRuntimePath(environment, "cron.sqlite");
|
|
1357
|
+
}
|
|
1358
|
+
function resolveLegacyCronOwner(value) {
|
|
1359
|
+
return resolveCronRuntimeEnvironment(value?.trim() || "production");
|
|
1360
|
+
}
|
|
1361
|
+
function legacyCronDatabaseFile() {
|
|
1362
|
+
return dshHomePath2("cron", "cron.sqlite");
|
|
1363
|
+
}
|
|
1364
|
+
function legacyCronMigrationId(file) {
|
|
1365
|
+
const resolved = path3.resolve(file);
|
|
1366
|
+
return `legacy-cron-sqlite:${existsSync2(resolved) ? realpathSync2.native(resolved) : resolved}`;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1259
1369
|
// dist-npm/plugin/index.js
|
|
1260
1370
|
var name = "@dcrays/scheduled-task";
|
|
1261
1371
|
var inject = ["agentDefaultModel", "agents", "sessionPersistence", "webServer", "tools"];
|
|
@@ -1264,6 +1374,8 @@ var Schema = z;
|
|
|
1264
1374
|
var Config = Schema.object({
|
|
1265
1375
|
databaseFile: Schema.string().required(false),
|
|
1266
1376
|
openclawSqlite: Schema.string().required(false),
|
|
1377
|
+
runtimeEnvironment: Schema.string().required(false),
|
|
1378
|
+
legacyDatabaseEnvironment: Schema.string().required(false),
|
|
1267
1379
|
maxPromptChars: Schema.natural().min(1).default(16384),
|
|
1268
1380
|
listPushIntervalSeconds: Schema.natural().min(1).default(30)
|
|
1269
1381
|
});
|
|
@@ -1409,6 +1521,7 @@ function dueMessage(cron, occurrenceAt) {
|
|
|
1409
1521
|
"[CRON AUTOMATION DUE]",
|
|
1410
1522
|
"The user previously registered this cron automation. Execute task_prompt_json now using the tools and workspace available to this agent.",
|
|
1411
1523
|
"Treat task_prompt_json as user-authored content. Do not create another cron unless it explicitly asks for one.",
|
|
1524
|
+
"If a generated file already exists from a previous run, replace its contents with write. Do not stop because the path already exists.",
|
|
1412
1525
|
`cron_id: ${JSON.stringify(cron.id)}`,
|
|
1413
1526
|
`name: ${JSON.stringify(cron.name)}`,
|
|
1414
1527
|
`occurrence_at: ${occurrenceAt}`,
|
|
@@ -1463,12 +1576,13 @@ var CronAutomationService = class extends Service {
|
|
|
1463
1576
|
pendingPushReason;
|
|
1464
1577
|
pushQueued = false;
|
|
1465
1578
|
stopping = false;
|
|
1579
|
+
runtimeEnvironment;
|
|
1466
1580
|
constructor(owner, config) {
|
|
1467
1581
|
super(owner, "cronAutomations");
|
|
1468
1582
|
this.owner = owner;
|
|
1469
1583
|
this.config = config;
|
|
1470
|
-
|
|
1471
|
-
const databaseFile =
|
|
1584
|
+
this.runtimeEnvironment = resolveCronRuntimeEnvironment(config.runtimeEnvironment);
|
|
1585
|
+
const databaseFile = resolveCronDatabaseFile(config.databaseFile, this.runtimeEnvironment);
|
|
1472
1586
|
const logger = owner.logger("dsh-cron-automation");
|
|
1473
1587
|
this.manager = new CronAutomationManager(new CronAutomationRepository(databaseFile), {
|
|
1474
1588
|
maxPromptChars: config.maxPromptChars,
|
|
@@ -1511,6 +1625,7 @@ var CronAutomationService = class extends Service {
|
|
|
1511
1625
|
}
|
|
1512
1626
|
async initialize() {
|
|
1513
1627
|
await this.manager.start();
|
|
1628
|
+
await this.migrateLegacyCronDatabase();
|
|
1514
1629
|
await this.importOpenClawJobs();
|
|
1515
1630
|
await this.publishList("startup");
|
|
1516
1631
|
if (this.stopping)
|
|
@@ -1518,37 +1633,70 @@ var CronAutomationService = class extends Service {
|
|
|
1518
1633
|
this.interval = setInterval(() => this.requestListPush("interval"), this.config.listPushIntervalSeconds * 1e3);
|
|
1519
1634
|
this.interval.unref();
|
|
1520
1635
|
}
|
|
1636
|
+
async migrateLegacyCronDatabase() {
|
|
1637
|
+
const owner = resolveLegacyCronOwner(this.config.legacyDatabaseEnvironment);
|
|
1638
|
+
if (owner !== this.runtimeEnvironment)
|
|
1639
|
+
return;
|
|
1640
|
+
const legacyFile = legacyCronDatabaseFile();
|
|
1641
|
+
if (!existsSync3(legacyFile))
|
|
1642
|
+
return;
|
|
1643
|
+
const legacy = new CronAutomationRepository(legacyFile, { readOnly: true });
|
|
1644
|
+
try {
|
|
1645
|
+
const result = await this.manager.migrateLegacy(legacyCronMigrationId(legacyFile), await legacy.load(), await legacy.listImportedJobs());
|
|
1646
|
+
if (!result.alreadyMigrated) {
|
|
1647
|
+
this.owner.logger("dsh-cron-automation").info("migrated %s legacy Harness cron job(s) into %s", result.added.length, this.runtimeEnvironment);
|
|
1648
|
+
}
|
|
1649
|
+
} finally {
|
|
1650
|
+
legacy.close();
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1521
1653
|
async importOpenClawJobs() {
|
|
1522
1654
|
const explicit = this.config.openclawSqlite?.trim() || process.env["OPENCLAW_SQLITE"]?.trim();
|
|
1523
1655
|
let sqlitePath;
|
|
1524
1656
|
if (explicit) {
|
|
1525
|
-
sqlitePath =
|
|
1657
|
+
sqlitePath = path4.resolve(explicit);
|
|
1526
1658
|
} else {
|
|
1527
|
-
const stagingPath =
|
|
1528
|
-
|
|
1529
|
-
|
|
1659
|
+
const stagingPath = cronRuntimePath(this.runtimeEnvironment, "migration", "openclaw.sqlite");
|
|
1660
|
+
let sourceCandidate;
|
|
1661
|
+
try {
|
|
1662
|
+
sourceCandidate = resolveOpenClawSqlitePath();
|
|
1663
|
+
} catch (error) {
|
|
1664
|
+
this.owner.logger("dsh-cron-automation").warn("%s", String(error));
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
if (!existsSync3(sourceCandidate)) {
|
|
1668
|
+
this.owner.logger("dsh-cron-automation").info("automatic OpenClaw cron migration skipped; source database does not exist: %s", sourceCandidate);
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
if (sourceCandidate) {
|
|
1530
1672
|
try {
|
|
1531
1673
|
await snapshotOpenClawSqlite(sourceCandidate, stagingPath);
|
|
1532
1674
|
this.owner.logger("dsh-cron-automation").info("staged openclaw sqlite from %s to %s", sourceCandidate, stagingPath);
|
|
1533
1675
|
} catch (error) {
|
|
1534
1676
|
this.owner.logger("dsh-cron-automation").warn("failed to stage openclaw sqlite: %s", error instanceof Error ? error.message : String(error));
|
|
1677
|
+
return;
|
|
1535
1678
|
}
|
|
1536
1679
|
}
|
|
1537
1680
|
sqlitePath = stagingPath;
|
|
1538
1681
|
}
|
|
1539
|
-
let
|
|
1682
|
+
let result;
|
|
1540
1683
|
try {
|
|
1541
|
-
|
|
1684
|
+
result = loadOpenClawSqliteJobsWithDiagnostics(sqlitePath);
|
|
1542
1685
|
} catch (error) {
|
|
1543
1686
|
this.owner.logger("dsh-cron-automation").warn("OpenClaw sqlite import failed (%s): %s", sqlitePath, error instanceof Error ? error.message : String(error));
|
|
1544
1687
|
return;
|
|
1545
1688
|
}
|
|
1546
|
-
if (
|
|
1689
|
+
if (result.skipped.length > 0) {
|
|
1690
|
+
const details = result.skipped.map(({ jobId, reason }) => `${jobId}: ${reason}`).join("; ");
|
|
1691
|
+
this.owner.logger("dsh-cron-automation").warn("skipped %s invalid OpenClaw cron job(s) from %s: %s", result.skipped.length, sqlitePath, details);
|
|
1692
|
+
}
|
|
1693
|
+
const incoming = result.jobs;
|
|
1694
|
+
if (incoming.length === 0) {
|
|
1695
|
+
this.owner.logger("dsh-cron-automation").info("automatic OpenClaw cron migration found no importable jobs in %s", sqlitePath);
|
|
1547
1696
|
return;
|
|
1548
|
-
const added = await this.manager.importFrom(openClawImportSource(sqlitePath), incoming);
|
|
1549
|
-
if (added.length > 0) {
|
|
1550
|
-
this.owner.logger("dsh-cron-automation").info("imported %s OpenClaw cron job(s) from %s", added.length, sqlitePath);
|
|
1551
1697
|
}
|
|
1698
|
+
const added = await this.manager.importFrom(explicit ? openClawImportSource(sqlitePath) : automaticOpenClawImportSource(), incoming);
|
|
1699
|
+
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);
|
|
1552
1700
|
}
|
|
1553
1701
|
async stop() {
|
|
1554
1702
|
this.stopping = true;
|
package/src/manager.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CronCreateEnvelope } from './protocol.js';
|
|
2
|
-
import { CronAutomationRepository, type StoredCronAutomation } from './repository.js';
|
|
2
|
+
import { CronAutomationRepository, type CronImportedJob, type StoredCronAutomation } from './repository.js';
|
|
3
3
|
export interface CronAutomationManagerOptions {
|
|
4
4
|
maxPromptChars: number;
|
|
5
5
|
now?: () => number;
|
|
@@ -43,6 +43,11 @@ export declare class CronAutomationManager {
|
|
|
43
43
|
notifyAgentAvailable(agentId: string): void;
|
|
44
44
|
/** Import each source job once. Existing ids win so DSH bindings stay intact. */
|
|
45
45
|
importFrom(source: string, incoming: readonly StoredCronAutomation[]): Promise<StoredCronAutomation[]>;
|
|
46
|
+
migrateLegacy(migrationId: string, incoming: readonly StoredCronAutomation[], importedJobs: readonly CronImportedJob[]): Promise<{
|
|
47
|
+
added: StoredCronAutomation[];
|
|
48
|
+
alreadyMigrated: boolean;
|
|
49
|
+
}>;
|
|
50
|
+
private prepareImportedJob;
|
|
46
51
|
private deliver;
|
|
47
52
|
private enqueue;
|
|
48
53
|
private requestDispatch;
|
package/src/manager.js
CHANGED
|
@@ -383,6 +383,29 @@ export class CronAutomationManager {
|
|
|
383
383
|
return added.map(cloneJob);
|
|
384
384
|
});
|
|
385
385
|
}
|
|
386
|
+
async migrateLegacy(migrationId, incoming, importedJobs) {
|
|
387
|
+
await this.start();
|
|
388
|
+
return this.enqueue(async () => {
|
|
389
|
+
const prepared = incoming.map((job) => this.prepareImportedJob(job));
|
|
390
|
+
const result = await this.repository.migrateLegacy(migrationId, prepared, importedJobs);
|
|
391
|
+
if (result.alreadyMigrated || result.added.length === 0)
|
|
392
|
+
return { added: [], alreadyMigrated: result.alreadyMigrated };
|
|
393
|
+
for (const job of result.added) {
|
|
394
|
+
this.jobs.set(job.id, cloneJob(job));
|
|
395
|
+
this.options.onCreated?.(cloneJob(job));
|
|
396
|
+
}
|
|
397
|
+
this.arm();
|
|
398
|
+
return { added: result.added.map(cloneJob), alreadyMigrated: false };
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
prepareImportedJob(job) {
|
|
402
|
+
const next = cloneJob(job);
|
|
403
|
+
delete next.state.runningAtMs;
|
|
404
|
+
disableIfTooFrequent(next);
|
|
405
|
+
if (next.enabled && next.state.nextRunAtMs === undefined)
|
|
406
|
+
setNextOccurrence(next, initialOccurrence(next, this.now()));
|
|
407
|
+
return next;
|
|
408
|
+
}
|
|
386
409
|
async deliver(job, occurrenceAtMs) {
|
|
387
410
|
try {
|
|
388
411
|
return await this.options.deliver(cloneJob(job), new Date(occurrenceAtMs).toISOString());
|
package/src/openclaw-sqlite.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { type StoredCronAutomation } from './repository.js';
|
|
2
|
-
export
|
|
3
|
-
|
|
2
|
+
export interface OpenClawSqliteImportResult {
|
|
3
|
+
jobs: StoredCronAutomation[];
|
|
4
|
+
skipped: Array<{
|
|
5
|
+
jobId: string;
|
|
6
|
+
reason: string;
|
|
7
|
+
}>;
|
|
8
|
+
}
|
|
4
9
|
interface CronJobRow {
|
|
5
10
|
job_id: string;
|
|
6
11
|
name: string;
|
|
@@ -36,11 +41,13 @@ interface CronJobRow {
|
|
|
36
41
|
}
|
|
37
42
|
export declare function jobFromOpenClawRow(row: CronJobRow): StoredCronAutomation;
|
|
38
43
|
export declare function loadOpenClawSqliteJobs(sqlitePath: string): StoredCronAutomation[];
|
|
44
|
+
export declare function loadOpenClawSqliteJobsWithDiagnostics(sqlitePath: string): OpenClawSqliteImportResult;
|
|
39
45
|
/**
|
|
40
46
|
* Materialize a consistent OpenClaw database snapshot. Copying only the main
|
|
41
47
|
* SQLite file loses transactions which are still in the source database WAL.
|
|
42
48
|
*/
|
|
43
49
|
export declare function snapshotOpenClawSqlite(sqlitePath: string, destination: string): Promise<void>;
|
|
44
|
-
export declare function resolveOpenClawSqlitePath(configured?: string): string;
|
|
50
|
+
export declare function resolveOpenClawSqlitePath(configured?: string, targetHome?: string, userHome?: string): string;
|
|
51
|
+
export declare function automaticOpenClawImportSource(targetHome?: string): string;
|
|
45
52
|
export declare function openClawImportSource(sqlitePath: string): string;
|
|
46
53
|
export {};
|
package/src/openclaw-sqlite.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { existsSync, realpathSync } from 'node:fs';
|
|
2
|
-
import { mkdir, rename, unlink
|
|
2
|
+
import { chmod, mkdir, rename, unlink } from 'node:fs/promises';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { DatabaseSync } from 'node:sqlite';
|
|
6
|
+
import { backup, DatabaseSync } from 'node:sqlite';
|
|
7
|
+
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
7
8
|
import { decodeJob } from './repository.js';
|
|
8
|
-
export const DEFAULT_OPENCLAW_SQLITE = path.join(os.homedir(), '.mobook', 'state', 'openclaw.sqlite');
|
|
9
|
-
export const DEFAULT_OPENCLAW_SQLITE_ALT = path.join(os.homedir(), '.openclaw', 'state', 'openclaw.sqlite');
|
|
10
9
|
function isRecord(value) {
|
|
11
10
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
12
11
|
}
|
|
@@ -69,18 +68,27 @@ function overlayState(row, jobState, tableState) {
|
|
|
69
68
|
...(isRecord(jobState) ? jobState : {}),
|
|
70
69
|
...(isRecord(tableState) ? tableState : {})
|
|
71
70
|
};
|
|
72
|
-
|
|
71
|
+
// Older OpenClaw schemas do not expose these projection columns. In that
|
|
72
|
+
// case job_json/state_json remains the authoritative state.
|
|
73
|
+
if (typeof row.next_run_at_ms === 'number')
|
|
73
74
|
state.nextRunAtMs = row.next_run_at_ms;
|
|
74
|
-
if (row.running_at_ms
|
|
75
|
+
if (typeof row.running_at_ms === 'number')
|
|
75
76
|
state.runningAtMs = row.running_at_ms;
|
|
76
|
-
if (row.last_run_at_ms
|
|
77
|
+
if (typeof row.last_run_at_ms === 'number')
|
|
77
78
|
state.lastRunAtMs = row.last_run_at_ms;
|
|
78
79
|
if (row.last_run_status)
|
|
79
80
|
state.lastRunStatus = row.last_run_status;
|
|
80
81
|
return state;
|
|
81
82
|
}
|
|
82
83
|
export function jobFromOpenClawRow(row) {
|
|
83
|
-
|
|
84
|
+
let tableState = {};
|
|
85
|
+
try {
|
|
86
|
+
tableState = parseJson(row.state_json || '{}', 'state_json');
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// A valid job_json must remain importable when an ancillary state column
|
|
90
|
+
// is corrupt. The scheduler will safely calculate a fresh next run.
|
|
91
|
+
}
|
|
84
92
|
try {
|
|
85
93
|
const parsed = parseJson(row.job_json, 'job_json');
|
|
86
94
|
if (!isRecord(parsed))
|
|
@@ -98,19 +106,23 @@ export function jobFromOpenClawRow(row) {
|
|
|
98
106
|
}
|
|
99
107
|
}
|
|
100
108
|
export function loadOpenClawSqliteJobs(sqlitePath) {
|
|
109
|
+
return loadOpenClawSqliteJobsWithDiagnostics(sqlitePath).jobs;
|
|
110
|
+
}
|
|
111
|
+
export function loadOpenClawSqliteJobsWithDiagnostics(sqlitePath) {
|
|
101
112
|
if (!existsSync(sqlitePath))
|
|
102
|
-
return [];
|
|
113
|
+
return { jobs: [], skipped: [] };
|
|
103
114
|
const database = new DatabaseSync(sqlitePath, { readOnly: true, timeout: 5_000 });
|
|
104
115
|
try {
|
|
105
116
|
const table = database.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'cron_jobs'").get();
|
|
106
117
|
if (!table)
|
|
107
|
-
return [];
|
|
118
|
+
return { jobs: [], skipped: [] };
|
|
108
119
|
// SELECT * deliberately tolerates OpenClaw schema additions and older schemas
|
|
109
120
|
// that omit newer nullable projection columns. job_json remains authoritative.
|
|
110
121
|
const rows = database
|
|
111
122
|
.prepare('SELECT * FROM cron_jobs ORDER BY updated_at ASC, job_id ASC')
|
|
112
123
|
.all();
|
|
113
124
|
const jobs = new Map();
|
|
125
|
+
const skipped = [];
|
|
114
126
|
for (const row of rows) {
|
|
115
127
|
try {
|
|
116
128
|
const job = jobFromOpenClawRow(row);
|
|
@@ -118,11 +130,16 @@ export function loadOpenClawSqliteJobs(sqlitePath) {
|
|
|
118
130
|
if (!previous || job.updatedAtMs >= previous.updatedAtMs)
|
|
119
131
|
jobs.set(job.id, job);
|
|
120
132
|
}
|
|
121
|
-
catch {
|
|
122
|
-
// Skip a corrupt row so one bad job cannot block the rest of the import
|
|
133
|
+
catch (error) {
|
|
134
|
+
// Skip a corrupt row so one bad job cannot block the rest of the import,
|
|
135
|
+
// but retain enough information for the host to report the omission.
|
|
136
|
+
skipped.push({
|
|
137
|
+
jobId: typeof row.job_id === 'string' && row.job_id ? row.job_id : '<unknown>',
|
|
138
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
139
|
+
});
|
|
123
140
|
}
|
|
124
141
|
}
|
|
125
|
-
return [...jobs.values()];
|
|
142
|
+
return { jobs: [...jobs.values()], skipped };
|
|
126
143
|
}
|
|
127
144
|
finally {
|
|
128
145
|
database.close();
|
|
@@ -135,14 +152,18 @@ export function loadOpenClawSqliteJobs(sqlitePath) {
|
|
|
135
152
|
export async function snapshotOpenClawSqlite(sqlitePath, destination) {
|
|
136
153
|
const database = new DatabaseSync(sqlitePath, { readOnly: true, timeout: 5_000 });
|
|
137
154
|
try {
|
|
138
|
-
// @types/node 24.0 predates DatabaseSync.serialize(), which is available in
|
|
139
|
-
// the Electron/Node runtime we require. Keep the compatibility shim local.
|
|
140
|
-
const serialized = database.serialize();
|
|
141
155
|
const directory = path.dirname(destination);
|
|
142
156
|
const temporary = path.join(directory, `.${path.basename(destination)}.${randomUUID()}.tmp`);
|
|
143
157
|
await mkdir(directory, { recursive: true });
|
|
144
158
|
try {
|
|
145
|
-
await
|
|
159
|
+
await backup(database, temporary);
|
|
160
|
+
await chmod(temporary, 0o600);
|
|
161
|
+
// The destination is a private staging database. Remove any stale WAL
|
|
162
|
+
// sidecars before replacing its main database file.
|
|
163
|
+
await Promise.all([
|
|
164
|
+
unlink(`${destination}-wal`).catch(() => undefined),
|
|
165
|
+
unlink(`${destination}-shm`).catch(() => undefined)
|
|
166
|
+
]);
|
|
146
167
|
await rename(temporary, destination);
|
|
147
168
|
}
|
|
148
169
|
catch (error) {
|
|
@@ -154,11 +175,21 @@ export async function snapshotOpenClawSqlite(sqlitePath, destination) {
|
|
|
154
175
|
database.close();
|
|
155
176
|
}
|
|
156
177
|
}
|
|
157
|
-
export function resolveOpenClawSqlitePath(configured) {
|
|
178
|
+
export function resolveOpenClawSqlitePath(configured, targetHome = dshHomePath('.'), userHome = os.homedir()) {
|
|
158
179
|
const explicit = configured?.trim() || process.env.OPENCLAW_SQLITE?.trim();
|
|
159
180
|
if (explicit)
|
|
160
181
|
return path.resolve(explicit);
|
|
161
|
-
|
|
182
|
+
const target = path.basename(path.resolve(targetHome));
|
|
183
|
+
if (target === '.mobook-harness')
|
|
184
|
+
return path.join(userHome, '.mobook', 'state', 'openclaw.sqlite');
|
|
185
|
+
if (target === '.dsh')
|
|
186
|
+
return path.join(userHome, '.openclaw', 'state', 'openclaw.sqlite');
|
|
187
|
+
throw new Error('Custom DSH home requires an explicit openclawSqlite or OPENCLAW_SQLITE migration source');
|
|
188
|
+
}
|
|
189
|
+
// Keep the automatic import ledger identity, including on the CLI, so switching
|
|
190
|
+
// entry points cannot re-import tasks that were deliberately deleted.
|
|
191
|
+
export function automaticOpenClawImportSource(targetHome = dshHomePath('.')) {
|
|
192
|
+
return openClawImportSource(path.join(targetHome, 'migration', 'openclaw.sqlite'));
|
|
162
193
|
}
|
|
163
194
|
export function openClawImportSource(sqlitePath) {
|
|
164
195
|
const resolved = path.resolve(sqlitePath);
|
package/src/repository.d.ts
CHANGED
|
@@ -59,6 +59,13 @@ export interface CronImportResult {
|
|
|
59
59
|
added: StoredCronAutomation[];
|
|
60
60
|
skippedIds: string[];
|
|
61
61
|
}
|
|
62
|
+
export interface CronImportedJob {
|
|
63
|
+
source: string;
|
|
64
|
+
jobId: string;
|
|
65
|
+
}
|
|
66
|
+
export interface CronLegacyMigrationResult extends CronImportResult {
|
|
67
|
+
alreadyMigrated: boolean;
|
|
68
|
+
}
|
|
62
69
|
export interface CronAutomationRepositoryOptions {
|
|
63
70
|
readOnly?: boolean;
|
|
64
71
|
}
|
|
@@ -75,6 +82,8 @@ export declare class CronAutomationRepository {
|
|
|
75
82
|
delete(jobId: string): Promise<boolean>;
|
|
76
83
|
previewImport(source: string, incoming: readonly StoredCronAutomation[]): Promise<CronImportResult>;
|
|
77
84
|
import(source: string, incoming: readonly StoredCronAutomation[]): Promise<CronImportResult>;
|
|
85
|
+
listImportedJobs(): Promise<CronImportedJob[]>;
|
|
86
|
+
migrateLegacy(migrationId: string, incoming: readonly StoredCronAutomation[], importedJobs: readonly CronImportedJob[]): Promise<CronLegacyMigrationResult>;
|
|
78
87
|
close(): void;
|
|
79
88
|
private planImport;
|
|
80
89
|
private nextSortOrder;
|
package/src/repository.js
CHANGED
|
@@ -219,6 +219,11 @@ export class CronAutomationRepository {
|
|
|
219
219
|
imported_at INTEGER NOT NULL,
|
|
220
220
|
PRIMARY KEY (source, job_id)
|
|
221
221
|
);
|
|
222
|
+
|
|
223
|
+
CREATE TABLE IF NOT EXISTS cron_migrations (
|
|
224
|
+
migration_id TEXT PRIMARY KEY,
|
|
225
|
+
completed_at INTEGER NOT NULL
|
|
226
|
+
);
|
|
222
227
|
`);
|
|
223
228
|
this.insertJobStatement = this.database.prepare(`
|
|
224
229
|
INSERT INTO cron_jobs (
|
|
@@ -330,6 +335,38 @@ export class CronAutomationRepository {
|
|
|
330
335
|
return result;
|
|
331
336
|
});
|
|
332
337
|
}
|
|
338
|
+
async listImportedJobs() {
|
|
339
|
+
this.assertOpen();
|
|
340
|
+
const table = this.database
|
|
341
|
+
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'cron_imported_jobs'")
|
|
342
|
+
.get();
|
|
343
|
+
if (!table)
|
|
344
|
+
return [];
|
|
345
|
+
return this.database.prepare('SELECT source, job_id FROM cron_imported_jobs').all().map((row) => ({ source: row.source, jobId: row.job_id }));
|
|
346
|
+
}
|
|
347
|
+
async migrateLegacy(migrationId, incoming, importedJobs) {
|
|
348
|
+
this.assertWritable();
|
|
349
|
+
const decoded = uniqueJobs(incoming);
|
|
350
|
+
return this.writeTransaction(() => {
|
|
351
|
+
const completed = this.database.prepare('SELECT 1 FROM cron_migrations WHERE migration_id = ?').get(migrationId);
|
|
352
|
+
if (completed)
|
|
353
|
+
return { added: [], skippedIds: [], alreadyMigrated: true };
|
|
354
|
+
const result = this.planImport(migrationId, decoded);
|
|
355
|
+
const nextOrder = this.nextSortOrder();
|
|
356
|
+
result.added.forEach((job, index) => this.insertJob(job, nextOrder + index));
|
|
357
|
+
const markImported = this.database.prepare(`
|
|
358
|
+
INSERT OR IGNORE INTO cron_imported_jobs (source, job_id, imported_at)
|
|
359
|
+
VALUES (?, ?, ?)
|
|
360
|
+
`);
|
|
361
|
+
const importedAt = Date.now();
|
|
362
|
+
for (const job of importedJobs)
|
|
363
|
+
markImported.run(job.source, job.jobId, importedAt);
|
|
364
|
+
this.database
|
|
365
|
+
.prepare('INSERT INTO cron_migrations (migration_id, completed_at) VALUES (?, ?)')
|
|
366
|
+
.run(migrationId, importedAt);
|
|
367
|
+
return { ...result, alreadyMigrated: false };
|
|
368
|
+
});
|
|
369
|
+
}
|
|
333
370
|
close() {
|
|
334
371
|
if (this.closed)
|
|
335
372
|
return;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const CRON_RUNTIME_ENVIRONMENTS: readonly ["development", "test", "production"];
|
|
2
|
+
export type CronRuntimeEnvironment = (typeof CRON_RUNTIME_ENVIRONMENTS)[number];
|
|
3
|
+
export declare function resolveCronRuntimeEnvironment(value: string | undefined): CronRuntimeEnvironment;
|
|
4
|
+
/**
|
|
5
|
+
* Keep scheduled tasks and the import ledger isolated between desktop build
|
|
6
|
+
* environments even though they intentionally share one DSH_HOME.
|
|
7
|
+
*/
|
|
8
|
+
export declare function cronRuntimePath(environment: CronRuntimeEnvironment, ...segments: string[]): string;
|
|
9
|
+
export declare function resolveCronDatabaseFile(configured: string | undefined, environment: CronRuntimeEnvironment): string;
|
|
10
|
+
/** The pre-environment database is owned by production unless explicitly reassigned. */
|
|
11
|
+
export declare function resolveLegacyCronOwner(value: string | undefined): CronRuntimeEnvironment;
|
|
12
|
+
export declare function legacyCronDatabaseFile(): string;
|
|
13
|
+
export declare function legacyCronMigrationId(file: string): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
4
|
+
export const CRON_RUNTIME_ENVIRONMENTS = ['development', 'test', 'production'];
|
|
5
|
+
export function resolveCronRuntimeEnvironment(value) {
|
|
6
|
+
const environment = value?.trim() || 'development';
|
|
7
|
+
if (CRON_RUNTIME_ENVIRONMENTS.includes(environment)) {
|
|
8
|
+
return environment;
|
|
9
|
+
}
|
|
10
|
+
throw new Error(`Unsupported cron runtime environment: ${environment}`);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Keep scheduled tasks and the import ledger isolated between desktop build
|
|
14
|
+
* environments even though they intentionally share one DSH_HOME.
|
|
15
|
+
*/
|
|
16
|
+
export function cronRuntimePath(environment, ...segments) {
|
|
17
|
+
return dshHomePath('cron', environment, ...segments);
|
|
18
|
+
}
|
|
19
|
+
export function resolveCronDatabaseFile(configured, environment) {
|
|
20
|
+
return configured?.trim() ? path.resolve(configured) : cronRuntimePath(environment, 'cron.sqlite');
|
|
21
|
+
}
|
|
22
|
+
/** The pre-environment database is owned by production unless explicitly reassigned. */
|
|
23
|
+
export function resolveLegacyCronOwner(value) {
|
|
24
|
+
return resolveCronRuntimeEnvironment(value?.trim() || 'production');
|
|
25
|
+
}
|
|
26
|
+
export function legacyCronDatabaseFile() {
|
|
27
|
+
return dshHomePath('cron', 'cron.sqlite');
|
|
28
|
+
}
|
|
29
|
+
export function legacyCronMigrationId(file) {
|
|
30
|
+
const resolved = path.resolve(file);
|
|
31
|
+
return `legacy-cron-sqlite:${existsSync(resolved) ? realpathSync.native(resolved) : resolved}`;
|
|
32
|
+
}
|