@dcrays/scheduled-task 0.3.5 → 0.3.6-beta.2

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.3.5",
3
+ "version": "0.3.6-beta.2",
4
4
  "description": "Persistent client-facing cron automation service for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "main": "plugin/index.js",
package/plugin/index.js CHANGED
@@ -1100,6 +1100,9 @@ var CronAutomationManager = class {
1100
1100
  if (patch.at !== void 0 && Date.parse(patch.at) <= now) {
1101
1101
  throw new CronAutomationInputError("invalid_envelope", "at must be in the future");
1102
1102
  }
1103
+ if (patch.enabled === true && next.schedule.kind === "at" && atTimestamp(next.schedule) <= now) {
1104
+ throw new CronAutomationInputError("invalid_envelope", "at must be in the future");
1105
+ }
1103
1106
  if (patch.enabled !== void 0) {
1104
1107
  next.enabled = patch.enabled;
1105
1108
  delete next.state.runningAtMs;
@@ -1328,7 +1331,11 @@ var CronAutomationManager = class {
1328
1331
  }
1329
1332
  this.blockedAgents.delete(dshAgentIdFor(job));
1330
1333
  this.blockedUntil.delete(dshAgentIdFor(job));
1331
- if (job.deleteAfterRun === true || job.schedule.kind === "at") {
1334
+ if (job.schedule.kind === "at") {
1335
+ job.enabled = false;
1336
+ setNextOccurrence(job, void 0);
1337
+ await this.repository.upsert([job]);
1338
+ } else if (job.deleteAfterRun === true) {
1332
1339
  this.jobs.delete(jobId);
1333
1340
  await this.repository.delete(jobId);
1334
1341
  } else {
@@ -1398,6 +1405,20 @@ import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
1398
1405
  function isRecord3(value) {
1399
1406
  return typeof value === "object" && value !== null && !Array.isArray(value);
1400
1407
  }
1408
+ var LegacySessionBindingError = class extends Error {
1409
+ };
1410
+ function decodeImportedJob(value) {
1411
+ if (isRecord3(value) && typeof value.sessionTarget === "string" && value.sessionTarget.startsWith("session:")) {
1412
+ const sessionKey = value.sessionTarget.slice("session:".length);
1413
+ if (!sessionKey.trim())
1414
+ throw new LegacySessionBindingError("OpenClaw cron session target is empty");
1415
+ if (value.sessionKey !== void 0 && value.sessionKey !== null && value.sessionKey !== "" && value.sessionKey !== sessionKey) {
1416
+ throw new LegacySessionBindingError("OpenClaw cron session target conflicts with sessionKey");
1417
+ }
1418
+ return decodeJob({ ...value, sessionTarget: "isolated", sessionKey });
1419
+ }
1420
+ return decodeJob(value);
1421
+ }
1401
1422
  function parseJson(raw, label) {
1402
1423
  try {
1403
1424
  return JSON.parse(raw);
@@ -1476,15 +1497,17 @@ function jobFromOpenClawRow(row) {
1476
1497
  const parsed = parseJson(row.job_json, "job_json");
1477
1498
  if (!isRecord3(parsed))
1478
1499
  throw new Error("job_json must be an object");
1479
- return decodeJob({
1500
+ return decodeImportedJob({
1480
1501
  ...parsed,
1481
1502
  id: typeof parsed.id === "string" && parsed.id ? parsed.id : row.job_id,
1482
1503
  updatedAtMs: typeof parsed.updatedAtMs === "number" ? parsed.updatedAtMs : row.updated_at,
1483
1504
  createdAtMs: typeof parsed.createdAtMs === "number" ? parsed.createdAtMs : row.created_at_ms,
1484
1505
  state: overlayState(row, parsed.state, tableState)
1485
1506
  });
1486
- } catch {
1487
- return decodeJob(jobFromColumns(row, overlayState(row, {}, tableState)));
1507
+ } catch (error) {
1508
+ if (error instanceof LegacySessionBindingError)
1509
+ throw error;
1510
+ return decodeImportedJob(jobFromColumns(row, overlayState(row, {}, tableState)));
1488
1511
  }
1489
1512
  }
1490
1513
  function loadOpenClawSqliteJobsWithDiagnostics(sqlitePath) {
package/src/manager.js CHANGED
@@ -382,6 +382,9 @@ export class CronAutomationManager {
382
382
  if (patch.at !== undefined && Date.parse(patch.at) <= now) {
383
383
  throw new CronAutomationInputError('invalid_envelope', 'at must be in the future');
384
384
  }
385
+ if (patch.enabled === true && next.schedule.kind === 'at' && atTimestamp(next.schedule) <= now) {
386
+ throw new CronAutomationInputError('invalid_envelope', 'at must be in the future');
387
+ }
385
388
  if (patch.enabled !== undefined) {
386
389
  next.enabled = patch.enabled;
387
390
  delete next.state.runningAtMs;
@@ -630,7 +633,12 @@ export class CronAutomationManager {
630
633
  }
631
634
  this.blockedAgents.delete(dshAgentIdFor(job));
632
635
  this.blockedUntil.delete(dshAgentIdFor(job));
633
- if (job.deleteAfterRun === true || job.schedule.kind === 'at') {
636
+ if (job.schedule.kind === 'at') {
637
+ job.enabled = false;
638
+ setNextOccurrence(job, undefined);
639
+ await this.repository.upsert([job]);
640
+ }
641
+ else if (job.deleteAfterRun === true) {
634
642
  this.jobs.delete(jobId);
635
643
  await this.repository.delete(jobId);
636
644
  }
@@ -9,6 +9,24 @@ import { decodeJob } from './repository.js';
9
9
  function isRecord(value) {
10
10
  return typeof value === 'object' && value !== null && !Array.isArray(value);
11
11
  }
12
+ class LegacySessionBindingError extends Error {
13
+ }
14
+ /** Translate legacy execution targets without changing the task's business conversation. */
15
+ function decodeImportedJob(value) {
16
+ if (isRecord(value) && typeof value.sessionTarget === 'string' && value.sessionTarget.startsWith('session:')) {
17
+ const sessionKey = value.sessionTarget.slice('session:'.length);
18
+ if (!sessionKey.trim())
19
+ throw new LegacySessionBindingError('OpenClaw cron session target is empty');
20
+ if (value.sessionKey !== undefined &&
21
+ value.sessionKey !== null &&
22
+ value.sessionKey !== '' &&
23
+ value.sessionKey !== sessionKey) {
24
+ throw new LegacySessionBindingError('OpenClaw cron session target conflicts with sessionKey');
25
+ }
26
+ return decodeJob({ ...value, sessionTarget: 'isolated', sessionKey });
27
+ }
28
+ return decodeJob(value);
29
+ }
12
30
  function parseJson(raw, label) {
13
31
  try {
14
32
  return JSON.parse(raw);
@@ -93,7 +111,7 @@ export function jobFromOpenClawRow(row) {
93
111
  const parsed = parseJson(row.job_json, 'job_json');
94
112
  if (!isRecord(parsed))
95
113
  throw new Error('job_json must be an object');
96
- return decodeJob({
114
+ return decodeImportedJob({
97
115
  ...parsed,
98
116
  id: typeof parsed.id === 'string' && parsed.id ? parsed.id : row.job_id,
99
117
  updatedAtMs: typeof parsed.updatedAtMs === 'number' ? parsed.updatedAtMs : row.updated_at,
@@ -101,8 +119,11 @@ export function jobFromOpenClawRow(row) {
101
119
  state: overlayState(row, parsed.state, tableState)
102
120
  });
103
121
  }
104
- catch {
105
- return decodeJob(jobFromColumns(row, overlayState(row, {}, tableState)));
122
+ catch (error) {
123
+ // Conflicting routing must not silently fall back to a different conversation.
124
+ if (error instanceof LegacySessionBindingError)
125
+ throw error;
126
+ return decodeImportedJob(jobFromColumns(row, overlayState(row, {}, tableState)));
106
127
  }
107
128
  }
108
129
  export function loadOpenClawSqliteJobs(sqlitePath) {