@dcrays/scheduled-task 0.1.0 → 0.1.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/package.json +8 -10
- package/plugin/index.js +58 -17
- package/src/manager.js +12 -2
- package/src/openclaw-sqlite.d.ts +7 -1
- package/src/openclaw-sqlite.js +56 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dcrays/scheduled-task",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Persistent client-facing cron automation service for DeepSeek Harness (production)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "plugin/index.js",
|
|
@@ -32,16 +32,14 @@
|
|
|
32
32
|
"engines": {
|
|
33
33
|
"node": ">=24.0.0"
|
|
34
34
|
},
|
|
35
|
-
"dependencies": {
|
|
36
|
-
"@deepseek-ai/dsh-home-paths": "0.1.1-rc.2",
|
|
37
|
-
"@deepseek-ai/dsh-llm": "0.1.1-rc.2",
|
|
38
|
-
"@deepseek-ai/schemastery": "^3.18.1"
|
|
39
|
-
},
|
|
40
35
|
"peerDependencies": {
|
|
41
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
42
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
43
|
-
"@deepseek-ai/dsh-
|
|
44
|
-
"@deepseek-ai/dsh-
|
|
36
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
37
|
+
"@deepseek-ai/dsh-agent": "^0.1.2-rc.1",
|
|
38
|
+
"@deepseek-ai/dsh-home-paths": "0.1.2-rc.1",
|
|
39
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.2-rc.1",
|
|
40
|
+
"@deepseek-ai/dsh-llm": "^0.1.2-rc.1",
|
|
41
|
+
"@deepseek-ai/dsh-tools": "^0.1.2-rc.1",
|
|
42
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
45
43
|
},
|
|
46
44
|
"publishConfig": {
|
|
47
45
|
"access": "public"
|
package/plugin/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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 existsSync2 } from "node:fs";
|
|
4
5
|
import path3 from "node:path";
|
|
5
6
|
import { Service } from "@deepseek-ai/cordis";
|
|
6
7
|
import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
|
|
@@ -8,7 +9,7 @@ import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
|
8
9
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
9
10
|
import z from "@deepseek-ai/schemastery";
|
|
10
11
|
|
|
11
|
-
//
|
|
12
|
+
// dist-npm/src/cron.js
|
|
12
13
|
var FIELD_COUNT = 5;
|
|
13
14
|
var MAX_SEARCH_DAYS = 366 * 8;
|
|
14
15
|
var GREGORIAN_CYCLE_DAYS = 146097;
|
|
@@ -208,10 +209,10 @@ function nextCronOccurrence(afterEpochMs, cron, timeZone) {
|
|
|
208
209
|
throw new CronExpressionError("cron expression has no occurrence within the next eight years");
|
|
209
210
|
}
|
|
210
211
|
|
|
211
|
-
//
|
|
212
|
+
// dist-npm/src/manager.js
|
|
212
213
|
import { createHash } from "node:crypto";
|
|
213
214
|
|
|
214
|
-
//
|
|
215
|
+
// dist-npm/src/protocol.js
|
|
215
216
|
var CRON_CREATE_TYPE = "dsh/cron.create";
|
|
216
217
|
var CRON_LIST_TYPE = "dsh/cron.list";
|
|
217
218
|
var CronAutomationInputError = class extends Error {
|
|
@@ -302,7 +303,7 @@ function parseCronEditorRequest(message) {
|
|
|
302
303
|
return { id, text };
|
|
303
304
|
}
|
|
304
305
|
|
|
305
|
-
//
|
|
306
|
+
// dist-npm/src/repository.js
|
|
306
307
|
import fs from "node:fs";
|
|
307
308
|
import path from "node:path";
|
|
308
309
|
import { DatabaseSync } from "node:sqlite";
|
|
@@ -681,7 +682,7 @@ var CronAutomationRepository = class {
|
|
|
681
682
|
}
|
|
682
683
|
};
|
|
683
684
|
|
|
684
|
-
//
|
|
685
|
+
// dist-npm/src/manager.js
|
|
685
686
|
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
686
687
|
function automationId(agentId, requestId) {
|
|
687
688
|
return `cron-${createHash("sha256").update(`${agentId}\0${requestId}`).digest("hex").slice(0, 24)}`;
|
|
@@ -692,6 +693,9 @@ function cloneJob(job) {
|
|
|
692
693
|
function dshAgentIdFor(job) {
|
|
693
694
|
return job.dshSessionId ?? job.agentId;
|
|
694
695
|
}
|
|
696
|
+
function hasDshSession(job) {
|
|
697
|
+
return typeof job.dshSessionId === "string" && job.dshSessionId.length > 0;
|
|
698
|
+
}
|
|
695
699
|
function timezoneFor(schedule) {
|
|
696
700
|
return schedule.tz ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC";
|
|
697
701
|
}
|
|
@@ -982,6 +986,8 @@ var CronAutomationManager = class {
|
|
|
982
986
|
return "not_found";
|
|
983
987
|
if (job.state.runningAtMs !== void 0)
|
|
984
988
|
return "already_running";
|
|
989
|
+
if (!hasDshSession(job))
|
|
990
|
+
return "delivery_unavailable";
|
|
985
991
|
const occurrenceAtMs = this.now();
|
|
986
992
|
job.state.runningAtMs = occurrenceAtMs;
|
|
987
993
|
job.updatedAtMs = occurrenceAtMs;
|
|
@@ -1061,7 +1067,7 @@ var CronAutomationManager = class {
|
|
|
1061
1067
|
clearTimeout(this.timer);
|
|
1062
1068
|
this.timer = void 0;
|
|
1063
1069
|
const now = this.now();
|
|
1064
|
-
const target = [...this.jobs.values()].filter((job) => job.enabled && job.state.runningAtMs === void 0 && !this.blockedAgents.has(dshAgentIdFor(job))).map((job) => job.state.nextRunAtMs).filter((value) => value !== void 0).reduce((earliest, candidate) => earliest === void 0 || candidate < earliest ? candidate : earliest, void 0);
|
|
1070
|
+
const target = [...this.jobs.values()].filter((job) => job.enabled && hasDshSession(job) && job.state.runningAtMs === void 0 && !this.blockedAgents.has(dshAgentIdFor(job))).map((job) => job.state.nextRunAtMs).filter((value) => value !== void 0).reduce((earliest, candidate) => earliest === void 0 || candidate < earliest ? candidate : earliest, void 0);
|
|
1065
1071
|
if (target === void 0)
|
|
1066
1072
|
return;
|
|
1067
1073
|
const delay = Math.max(0, Math.min(target - now, MAX_TIMER_DELAY_MS));
|
|
@@ -1075,7 +1081,7 @@ var CronAutomationManager = class {
|
|
|
1075
1081
|
if (this.stopping)
|
|
1076
1082
|
return;
|
|
1077
1083
|
const now = this.now();
|
|
1078
|
-
const due = [...this.jobs.values()].filter((job) => job.enabled && job.state.nextRunAtMs !== void 0 && job.state.nextRunAtMs <= now).sort((left, right) => (left.state.nextRunAtMs ?? 0) - (right.state.nextRunAtMs ?? 0) || left.createdAtMs - right.createdAtMs);
|
|
1084
|
+
const due = [...this.jobs.values()].filter((job) => job.enabled && hasDshSession(job) && job.state.nextRunAtMs !== void 0 && job.state.nextRunAtMs <= now).sort((left, right) => (left.state.nextRunAtMs ?? 0) - (right.state.nextRunAtMs ?? 0) || left.createdAtMs - right.createdAtMs);
|
|
1079
1085
|
for (const job of due) {
|
|
1080
1086
|
const occurrenceAtMs = job.state.nextRunAtMs ?? now;
|
|
1081
1087
|
job.state.runningAtMs = now;
|
|
@@ -1105,12 +1111,15 @@ var CronAutomationManager = class {
|
|
|
1105
1111
|
}
|
|
1106
1112
|
};
|
|
1107
1113
|
|
|
1108
|
-
//
|
|
1114
|
+
// dist-npm/src/openclaw-sqlite.js
|
|
1109
1115
|
import { existsSync, realpathSync } from "node:fs";
|
|
1116
|
+
import { mkdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
1117
|
+
import { randomUUID } from "node:crypto";
|
|
1110
1118
|
import os from "node:os";
|
|
1111
1119
|
import path2 from "node:path";
|
|
1112
1120
|
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
1113
1121
|
var DEFAULT_OPENCLAW_SQLITE = path2.join(os.homedir(), ".mobook", "state", "openclaw.sqlite");
|
|
1122
|
+
var DEFAULT_OPENCLAW_SQLITE_ALT = path2.join(os.homedir(), ".openclaw", "state", "openclaw.sqlite");
|
|
1114
1123
|
function isRecord3(value) {
|
|
1115
1124
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1116
1125
|
}
|
|
@@ -1223,11 +1232,23 @@ function loadOpenClawSqliteJobs(sqlitePath) {
|
|
|
1223
1232
|
database.close();
|
|
1224
1233
|
}
|
|
1225
1234
|
}
|
|
1226
|
-
function
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1235
|
+
async function snapshotOpenClawSqlite(sqlitePath, destination) {
|
|
1236
|
+
const database = new DatabaseSync2(sqlitePath, { readOnly: true, timeout: 5e3 });
|
|
1237
|
+
try {
|
|
1238
|
+
const serialized = database.serialize();
|
|
1239
|
+
const directory = path2.dirname(destination);
|
|
1240
|
+
const temporary = path2.join(directory, `.${path2.basename(destination)}.${randomUUID()}.tmp`);
|
|
1241
|
+
await mkdir(directory, { recursive: true });
|
|
1242
|
+
try {
|
|
1243
|
+
await writeFile(temporary, serialized, { mode: 384 });
|
|
1244
|
+
await rename(temporary, destination);
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
await unlink(temporary).catch(() => void 0);
|
|
1247
|
+
throw error;
|
|
1248
|
+
}
|
|
1249
|
+
} finally {
|
|
1250
|
+
database.close();
|
|
1251
|
+
}
|
|
1231
1252
|
}
|
|
1232
1253
|
function openClawImportSource(sqlitePath) {
|
|
1233
1254
|
const resolved = path2.resolve(sqlitePath);
|
|
@@ -1235,7 +1256,7 @@ function openClawImportSource(sqlitePath) {
|
|
|
1235
1256
|
return `openclaw-sqlite:${canonical}`;
|
|
1236
1257
|
}
|
|
1237
1258
|
|
|
1238
|
-
//
|
|
1259
|
+
// dist-npm/plugin/index.js
|
|
1239
1260
|
var name = "@dcrays/scheduled-task";
|
|
1240
1261
|
var inject = ["agentDefaultModel", "agents", "sessionPersistence", "webServer", "tools"];
|
|
1241
1262
|
var API_PREFIX = "/api/cron-automations";
|
|
@@ -1452,9 +1473,13 @@ var CronAutomationService = class extends Service {
|
|
|
1452
1473
|
this.manager = new CronAutomationManager(new CronAutomationRepository(databaseFile), {
|
|
1453
1474
|
maxPromptChars: config.maxPromptChars,
|
|
1454
1475
|
deliver: async (cron, occurrenceAt) => {
|
|
1476
|
+
if (!cron.dshSessionId) {
|
|
1477
|
+
logger.warn("cron %s is waiting for an MBHChat DSH session binding", cron.id);
|
|
1478
|
+
return false;
|
|
1479
|
+
}
|
|
1455
1480
|
let agent;
|
|
1456
1481
|
try {
|
|
1457
|
-
agent = await resumeAgent(owner, cron.dshSessionId
|
|
1482
|
+
agent = await resumeAgent(owner, cron.dshSessionId);
|
|
1458
1483
|
} catch (error) {
|
|
1459
1484
|
logger.warn("could not resume agent %s for cron %s: %s", cron.agentId, cron.id, error instanceof Error ? error.message : String(error));
|
|
1460
1485
|
return false;
|
|
@@ -1494,7 +1519,23 @@ var CronAutomationService = class extends Service {
|
|
|
1494
1519
|
this.interval.unref();
|
|
1495
1520
|
}
|
|
1496
1521
|
async importOpenClawJobs() {
|
|
1497
|
-
const
|
|
1522
|
+
const explicit = this.config.openclawSqlite?.trim() || process.env["OPENCLAW_SQLITE"]?.trim();
|
|
1523
|
+
let sqlitePath;
|
|
1524
|
+
if (explicit) {
|
|
1525
|
+
sqlitePath = path3.resolve(explicit);
|
|
1526
|
+
} else {
|
|
1527
|
+
const stagingPath = dshHomePath("migration", "openclaw.sqlite");
|
|
1528
|
+
const sourceCandidate = [DEFAULT_OPENCLAW_SQLITE, DEFAULT_OPENCLAW_SQLITE_ALT].find(existsSync2);
|
|
1529
|
+
if (!existsSync2(stagingPath) && sourceCandidate) {
|
|
1530
|
+
try {
|
|
1531
|
+
await snapshotOpenClawSqlite(sourceCandidate, stagingPath);
|
|
1532
|
+
this.owner.logger("dsh-cron-automation").info("staged openclaw sqlite from %s to %s", sourceCandidate, stagingPath);
|
|
1533
|
+
} catch (error) {
|
|
1534
|
+
this.owner.logger("dsh-cron-automation").warn("failed to stage openclaw sqlite: %s", error instanceof Error ? error.message : String(error));
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
sqlitePath = stagingPath;
|
|
1538
|
+
}
|
|
1498
1539
|
let incoming;
|
|
1499
1540
|
try {
|
|
1500
1541
|
incoming = loadOpenClawSqliteJobs(sqlitePath);
|
package/src/manager.js
CHANGED
|
@@ -12,6 +12,9 @@ function cloneJob(job) {
|
|
|
12
12
|
function dshAgentIdFor(job) {
|
|
13
13
|
return job.dshSessionId ?? job.agentId;
|
|
14
14
|
}
|
|
15
|
+
function hasDshSession(job) {
|
|
16
|
+
return typeof job.dshSessionId === 'string' && job.dshSessionId.length > 0;
|
|
17
|
+
}
|
|
15
18
|
function timezoneFor(schedule) {
|
|
16
19
|
return schedule.tz ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? 'UTC';
|
|
17
20
|
}
|
|
@@ -317,6 +320,10 @@ export class CronAutomationManager {
|
|
|
317
320
|
return 'not_found';
|
|
318
321
|
if (job.state.runningAtMs !== undefined)
|
|
319
322
|
return 'already_running';
|
|
323
|
+
// Imported OpenClaw jobs refer to an OpenClaw agent id, not a DSH session.
|
|
324
|
+
// They must wait until mbh-chat supplies a verified DSH session binding.
|
|
325
|
+
if (!hasDshSession(job))
|
|
326
|
+
return 'delivery_unavailable';
|
|
320
327
|
const occurrenceAtMs = this.now();
|
|
321
328
|
job.state.runningAtMs = occurrenceAtMs;
|
|
322
329
|
job.updatedAtMs = occurrenceAtMs;
|
|
@@ -401,7 +408,10 @@ export class CronAutomationManager {
|
|
|
401
408
|
this.timer = undefined;
|
|
402
409
|
const now = this.now();
|
|
403
410
|
const target = [...this.jobs.values()]
|
|
404
|
-
.filter((job) => job.enabled &&
|
|
411
|
+
.filter((job) => job.enabled &&
|
|
412
|
+
hasDshSession(job) &&
|
|
413
|
+
job.state.runningAtMs === undefined &&
|
|
414
|
+
!this.blockedAgents.has(dshAgentIdFor(job)))
|
|
405
415
|
.map((job) => job.state.nextRunAtMs)
|
|
406
416
|
.filter((value) => value !== undefined)
|
|
407
417
|
.reduce((earliest, candidate) => (earliest === undefined || candidate < earliest ? candidate : earliest), undefined);
|
|
@@ -419,7 +429,7 @@ export class CronAutomationManager {
|
|
|
419
429
|
return;
|
|
420
430
|
const now = this.now();
|
|
421
431
|
const due = [...this.jobs.values()]
|
|
422
|
-
.filter((job) => job.enabled && job.state.nextRunAtMs !== undefined && job.state.nextRunAtMs <= now)
|
|
432
|
+
.filter((job) => job.enabled && hasDshSession(job) && job.state.nextRunAtMs !== undefined && job.state.nextRunAtMs <= now)
|
|
423
433
|
.sort((left, right) => (left.state.nextRunAtMs ?? 0) - (right.state.nextRunAtMs ?? 0) || left.createdAtMs - right.createdAtMs);
|
|
424
434
|
for (const job of due) {
|
|
425
435
|
const occurrenceAtMs = job.state.nextRunAtMs ?? now;
|
package/src/openclaw-sqlite.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { type StoredCronAutomation } from
|
|
1
|
+
import { type StoredCronAutomation } from './repository.js';
|
|
2
2
|
export declare const DEFAULT_OPENCLAW_SQLITE: string;
|
|
3
|
+
export declare const DEFAULT_OPENCLAW_SQLITE_ALT: string;
|
|
3
4
|
interface CronJobRow {
|
|
4
5
|
job_id: string;
|
|
5
6
|
name: string;
|
|
@@ -35,6 +36,11 @@ interface CronJobRow {
|
|
|
35
36
|
}
|
|
36
37
|
export declare function jobFromOpenClawRow(row: CronJobRow): StoredCronAutomation;
|
|
37
38
|
export declare function loadOpenClawSqliteJobs(sqlitePath: string): StoredCronAutomation[];
|
|
39
|
+
/**
|
|
40
|
+
* Materialize a consistent OpenClaw database snapshot. Copying only the main
|
|
41
|
+
* SQLite file loses transactions which are still in the source database WAL.
|
|
42
|
+
*/
|
|
43
|
+
export declare function snapshotOpenClawSqlite(sqlitePath: string, destination: string): Promise<void>;
|
|
38
44
|
export declare function resolveOpenClawSqlitePath(configured?: string): string;
|
|
39
45
|
export declare function openClawImportSource(sqlitePath: string): string;
|
|
40
46
|
export {};
|
package/src/openclaw-sqlite.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { existsSync, realpathSync } from
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { mkdir, rename, unlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
7
|
+
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');
|
|
7
10
|
function isRecord(value) {
|
|
8
|
-
return typeof value ===
|
|
11
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
9
12
|
}
|
|
10
13
|
function parseJson(raw, label) {
|
|
11
14
|
try {
|
|
@@ -16,18 +19,18 @@ function parseJson(raw, label) {
|
|
|
16
19
|
}
|
|
17
20
|
}
|
|
18
21
|
function scheduleFromRow(row) {
|
|
19
|
-
if (row.schedule_kind ===
|
|
22
|
+
if (row.schedule_kind === 'every') {
|
|
20
23
|
return {
|
|
21
|
-
kind:
|
|
24
|
+
kind: 'every',
|
|
22
25
|
everyMs: row.every_ms,
|
|
23
26
|
...(row.anchor_ms === null ? {} : { anchorMs: row.anchor_ms })
|
|
24
27
|
};
|
|
25
28
|
}
|
|
26
|
-
if (row.schedule_kind ===
|
|
27
|
-
return { kind:
|
|
29
|
+
if (row.schedule_kind === 'at') {
|
|
30
|
+
return { kind: 'at', ...(row.at === null ? {} : { at: row.at }) };
|
|
28
31
|
}
|
|
29
32
|
return {
|
|
30
|
-
kind:
|
|
33
|
+
kind: 'cron',
|
|
31
34
|
expr: row.schedule_expr,
|
|
32
35
|
...(row.schedule_tz === null ? {} : { tz: row.schedule_tz })
|
|
33
36
|
};
|
|
@@ -42,17 +45,17 @@ function jobFromColumns(row, state) {
|
|
|
42
45
|
updatedAtMs: row.updated_at,
|
|
43
46
|
schedule: scheduleFromRow(row),
|
|
44
47
|
payload: {
|
|
45
|
-
kind: row.payload_kind ||
|
|
46
|
-
message: row.payload_message ??
|
|
48
|
+
kind: row.payload_kind || 'agentTurn',
|
|
49
|
+
message: row.payload_message ?? '',
|
|
47
50
|
...(row.payload_timeout_seconds === null ? {} : { timeoutSeconds: row.payload_timeout_seconds })
|
|
48
51
|
},
|
|
49
|
-
agentId: row.agent_id ??
|
|
52
|
+
agentId: row.agent_id ?? 'main',
|
|
50
53
|
...(row.session_key ? { sessionKey: row.session_key } : {}),
|
|
51
|
-
sessionTarget: row.session_target ||
|
|
52
|
-
wakeMode: row.wake_mode ||
|
|
54
|
+
sessionTarget: row.session_target || 'isolated',
|
|
55
|
+
wakeMode: row.wake_mode || 'now',
|
|
53
56
|
...(row.delete_after_run === 1 ? { deleteAfterRun: true } : {}),
|
|
54
57
|
delivery: {
|
|
55
|
-
mode: row.delivery_mode ??
|
|
58
|
+
mode: row.delivery_mode ?? 'announce',
|
|
56
59
|
...(row.delivery_channel ? { channel: row.delivery_channel } : {}),
|
|
57
60
|
...(row.delivery_to ? { to: row.delivery_to } : {}),
|
|
58
61
|
...(row.delivery_account_id ? { accountId: row.delivery_account_id } : {}),
|
|
@@ -77,16 +80,16 @@ function overlayState(row, jobState, tableState) {
|
|
|
77
80
|
return state;
|
|
78
81
|
}
|
|
79
82
|
export function jobFromOpenClawRow(row) {
|
|
80
|
-
const tableState = parseJson(row.state_json ||
|
|
83
|
+
const tableState = parseJson(row.state_json || '{}', 'state_json');
|
|
81
84
|
try {
|
|
82
|
-
const parsed = parseJson(row.job_json,
|
|
85
|
+
const parsed = parseJson(row.job_json, 'job_json');
|
|
83
86
|
if (!isRecord(parsed))
|
|
84
|
-
throw new Error(
|
|
87
|
+
throw new Error('job_json must be an object');
|
|
85
88
|
return decodeJob({
|
|
86
89
|
...parsed,
|
|
87
|
-
id: typeof parsed.id ===
|
|
88
|
-
updatedAtMs: typeof parsed.updatedAtMs ===
|
|
89
|
-
createdAtMs: typeof parsed.createdAtMs ===
|
|
90
|
+
id: typeof parsed.id === 'string' && parsed.id ? parsed.id : row.job_id,
|
|
91
|
+
updatedAtMs: typeof parsed.updatedAtMs === 'number' ? parsed.updatedAtMs : row.updated_at,
|
|
92
|
+
createdAtMs: typeof parsed.createdAtMs === 'number' ? parsed.createdAtMs : row.created_at_ms,
|
|
90
93
|
state: overlayState(row, parsed.state, tableState)
|
|
91
94
|
});
|
|
92
95
|
}
|
|
@@ -104,7 +107,9 @@ export function loadOpenClawSqliteJobs(sqlitePath) {
|
|
|
104
107
|
return [];
|
|
105
108
|
// SELECT * deliberately tolerates OpenClaw schema additions and older schemas
|
|
106
109
|
// that omit newer nullable projection columns. job_json remains authoritative.
|
|
107
|
-
const rows = database
|
|
110
|
+
const rows = database
|
|
111
|
+
.prepare('SELECT * FROM cron_jobs ORDER BY updated_at ASC, job_id ASC')
|
|
112
|
+
.all();
|
|
108
113
|
const jobs = new Map();
|
|
109
114
|
for (const row of rows) {
|
|
110
115
|
try {
|
|
@@ -123,6 +128,32 @@ export function loadOpenClawSqliteJobs(sqlitePath) {
|
|
|
123
128
|
database.close();
|
|
124
129
|
}
|
|
125
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Materialize a consistent OpenClaw database snapshot. Copying only the main
|
|
133
|
+
* SQLite file loses transactions which are still in the source database WAL.
|
|
134
|
+
*/
|
|
135
|
+
export async function snapshotOpenClawSqlite(sqlitePath, destination) {
|
|
136
|
+
const database = new DatabaseSync(sqlitePath, { readOnly: true, timeout: 5_000 });
|
|
137
|
+
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
|
+
const directory = path.dirname(destination);
|
|
142
|
+
const temporary = path.join(directory, `.${path.basename(destination)}.${randomUUID()}.tmp`);
|
|
143
|
+
await mkdir(directory, { recursive: true });
|
|
144
|
+
try {
|
|
145
|
+
await writeFile(temporary, serialized, { mode: 0o600 });
|
|
146
|
+
await rename(temporary, destination);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
await unlink(temporary).catch(() => undefined);
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
database.close();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
126
157
|
export function resolveOpenClawSqlitePath(configured) {
|
|
127
158
|
const explicit = configured?.trim() || process.env.OPENCLAW_SQLITE?.trim();
|
|
128
159
|
if (explicit)
|