@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcrays/scheduled-task",
3
- "version": "0.1.0",
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.1",
42
- "@deepseek-ai/dsh-agent": "^0.1.1-rc.2",
43
- "@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.2",
44
- "@deepseek-ai/dsh-tools": "^0.1.1-rc.2"
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
- // plugins/scheduled-task/dist-npm/plugin/index.js
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
- // plugins/scheduled-task/dist-npm/src/cron.js
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
- // plugins/scheduled-task/dist-npm/src/manager.js
212
+ // dist-npm/src/manager.js
212
213
  import { createHash } from "node:crypto";
213
214
 
214
- // plugins/scheduled-task/dist-npm/src/protocol.js
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
- // plugins/scheduled-task/dist-npm/src/repository.js
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
- // plugins/scheduled-task/dist-npm/src/manager.js
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
- // plugins/scheduled-task/dist-npm/src/openclaw-sqlite.js
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 resolveOpenClawSqlitePath(configured) {
1227
- const explicit = configured?.trim() || process.env.OPENCLAW_SQLITE?.trim();
1228
- if (explicit)
1229
- return path2.resolve(explicit);
1230
- return DEFAULT_OPENCLAW_SQLITE;
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
- // plugins/scheduled-task/dist-npm/plugin/index.js
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 ?? cron.agentId);
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 sqlitePath = resolveOpenClawSqlitePath(this.config.openclawSqlite);
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 && job.state.runningAtMs === undefined && !this.blockedAgents.has(dshAgentIdFor(job)))
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;
@@ -1,5 +1,6 @@
1
- import { type StoredCronAutomation } from "./repository.js";
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 {};
@@ -1,11 +1,14 @@
1
- import { existsSync, realpathSync } from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { DatabaseSync } from "node:sqlite";
5
- import { decodeJob } from "./repository.js";
6
- export const DEFAULT_OPENCLAW_SQLITE = path.join(os.homedir(), ".mobook", "state", "openclaw.sqlite");
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 === "object" && value !== null && !Array.isArray(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 === "every") {
22
+ if (row.schedule_kind === 'every') {
20
23
  return {
21
- kind: "every",
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 === "at") {
27
- return { kind: "at", ...(row.at === null ? {} : { at: row.at }) };
29
+ if (row.schedule_kind === 'at') {
30
+ return { kind: 'at', ...(row.at === null ? {} : { at: row.at }) };
28
31
  }
29
32
  return {
30
- kind: "cron",
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 || "agentTurn",
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 ?? "main",
52
+ agentId: row.agent_id ?? 'main',
50
53
  ...(row.session_key ? { sessionKey: row.session_key } : {}),
51
- sessionTarget: row.session_target || "isolated",
52
- wakeMode: row.wake_mode || "now",
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 ?? "announce",
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 || "{}", "state_json");
83
+ const tableState = parseJson(row.state_json || '{}', 'state_json');
81
84
  try {
82
- const parsed = parseJson(row.job_json, "job_json");
85
+ const parsed = parseJson(row.job_json, 'job_json');
83
86
  if (!isRecord(parsed))
84
- throw new Error("job_json must be an object");
87
+ throw new Error('job_json must be an object');
85
88
  return decodeJob({
86
89
  ...parsed,
87
- id: typeof parsed.id === "string" && parsed.id ? parsed.id : row.job_id,
88
- updatedAtMs: typeof parsed.updatedAtMs === "number" ? parsed.updatedAtMs : row.updated_at,
89
- createdAtMs: typeof parsed.createdAtMs === "number" ? parsed.createdAtMs : row.created_at_ms,
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.prepare("SELECT * FROM cron_jobs ORDER BY updated_at ASC, job_id ASC").all();
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)