openclaw-scheduler 0.2.9 → 0.2.10

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.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * migrate-consolidate.js -- Single idempotent migration for existing databases
3
3
  *
4
- * Brings any DB from any prior version up to the current schema (v23).
4
+ * Brings any DB from any prior version up to the current schema (v24).
5
5
  * Fresh installs get everything from schema.sql directly -- this only
6
6
  * runs ALTER TABLEs needed for DBs created before the current schema.
7
7
  *
@@ -58,6 +58,7 @@ export default function migrateConsolidate() {
58
58
  'max_trigger_fanout', 'output_store_limit_bytes',
59
59
  'output_excerpt_limit_bytes', 'output_summary_limit_bytes',
60
60
  'output_offload_threshold_bytes', 'ttl_hours', 'auth_profile',
61
+ 'payload_model_fallback', 'auth_profile_fallback',
61
62
  'schedule_kind', 'schedule_at', 'delivery_channel', 'delivery_to',
62
63
  'delivery_opt_out_reason', 'origin', 'parent_id', 'created_at',
63
64
  'updated_at', 'delete_after_run', 'next_run_at', 'last_run_at',
@@ -137,7 +138,7 @@ export default function migrateConsolidate() {
137
138
  `).get()?.cnt ?? 0)
138
139
  : 0;
139
140
  if (
140
- current >= 23
141
+ current >= 24
141
142
  && hasLatestColumns
142
143
  && legacyAtIsoCount === 0
143
144
  && legacyPayloadMismatchCount === 0
@@ -345,6 +346,9 @@ export default function migrateConsolidate() {
345
346
  `ALTER TABLE runs ADD COLUMN credential_handoff_summary TEXT DEFAULT NULL`,
346
347
  // v23: child credential policy
347
348
  `ALTER TABLE jobs ADD COLUMN child_credential_policy TEXT DEFAULT NULL`,
349
+ // v24: explicit fallback model/auth selection
350
+ `ALTER TABLE jobs ADD COLUMN payload_model_fallback TEXT`,
351
+ `ALTER TABLE jobs ADD COLUMN auth_profile_fallback TEXT DEFAULT NULL`,
348
352
  ];
349
353
 
350
354
  for (const sql of alters) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-scheduler",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "description": "SQLite-backed job scheduler and workflow engine for OpenClaw agents",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -17,7 +17,7 @@
17
17
  "./package.json": "./package.json"
18
18
  },
19
19
  "engines": {
20
- "node": ">=22"
20
+ "node": "22.x || 24.x || 26.x"
21
21
  },
22
22
  "scripts": {
23
23
  "start": "node dispatcher.js",
@@ -123,7 +123,7 @@
123
123
  },
124
124
  "homepage": "https://github.com/amittell/openclaw-scheduler#readme",
125
125
  "dependencies": {
126
- "better-sqlite3": "^11.10.0",
126
+ "better-sqlite3": "^12.10.0",
127
127
  "croner": "^10.0.1"
128
128
  },
129
129
  "devDependencies": {
package/paths.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { accessSync, constants, existsSync, mkdirSync } from 'fs';
2
- import { homedir } from 'os';
2
+ import { homedir, tmpdir } from 'os';
3
3
  import { join, dirname } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
 
@@ -25,6 +25,17 @@ function isNodeModulesInstall(moduleDir) {
25
25
  return /[\\/]node_modules[\\/](?:@[^\\/]+[\\/])?openclaw-scheduler(?:[\\/]|$)/.test(moduleDir);
26
26
  }
27
27
 
28
+ function isUsableWorkingDirectory(dirPath) {
29
+ const candidate = firstNonEmpty(dirPath);
30
+ if (!candidate) return false;
31
+ try {
32
+ accessSync(candidate, constants.R_OK | constants.X_OK);
33
+ return true;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
28
39
  export function resolveSchedulerHome(env = process.env) {
29
40
  const explicitHome = firstNonEmpty(env.SCHEDULER_HOME);
30
41
  if (explicitHome) return explicitHome;
@@ -62,6 +73,37 @@ export function resolveBackupStagingDir(env = process.env) {
62
73
  return join(resolveSchedulerHome(env), '.backup-staging');
63
74
  }
64
75
 
76
+ export function resolveServiceWorkingDirectory(params = {}) {
77
+ const env = params.env || process.env;
78
+ const explicitPath = firstNonEmpty(params.explicitPath);
79
+ if (explicitPath) {
80
+ try {
81
+ mkdirSync(explicitPath, { recursive: true });
82
+ if (isUsableWorkingDirectory(explicitPath)) return explicitPath;
83
+ } catch {
84
+ // Fall through to install-root/scheduler-home heuristics.
85
+ }
86
+ }
87
+
88
+ const moduleDir = firstNonEmpty(params.moduleDir) || __dirname;
89
+ if (!isNodeModulesInstall(moduleDir) && isUsableWorkingDirectory(moduleDir)) {
90
+ return moduleDir;
91
+ }
92
+
93
+ const schedulerHome = resolveSchedulerHome(env);
94
+ try {
95
+ mkdirSync(schedulerHome, { recursive: true });
96
+ if (isUsableWorkingDirectory(schedulerHome)) return schedulerHome;
97
+ } catch {
98
+ // Fall through to other safe directories.
99
+ }
100
+
101
+ const home = firstNonEmpty(env.HOME) || homedir();
102
+ if (isUsableWorkingDirectory(home)) return home;
103
+
104
+ return tmpdir();
105
+ }
106
+
65
107
  export function resolveArtifactsDir(params = {}) {
66
108
  const env = params.env || process.env;
67
109
  const explicit = firstNonEmpty(params.explicitPath) || firstNonEmpty(env.SCHEDULER_ARTIFACTS_DIR);
@@ -11,6 +11,7 @@ export const SCHEDULER_SCHEMAS = {
11
11
  payload_kind: { type: 'string', enum: ['systemEvent', 'agentTurn', 'shellCommand'] },
12
12
  payload_message: { type: 'string', maxLength: 100000 },
13
13
  payload_model: { type: 'string', nullable: true },
14
+ payload_model_fallback: { type: 'string', nullable: true, description: 'Optional fallback model override for a same-run retry after primary selection failure' },
14
15
  payload_thinking: { type: 'string', nullable: true },
15
16
  payload_timeout_seconds: { type: 'integer', min: 1, default: 120 },
16
17
  execution_intent: { type: 'string', enum: ['execute', 'plan'], default: 'execute' },
@@ -45,6 +46,7 @@ export const SCHEDULER_SCHEMAS = {
45
46
  output_offload_threshold_bytes: { type: 'integer', min: 128, default: 65536 },
46
47
  preferred_session_key: { type: 'string', nullable: true },
47
48
  auth_profile: { type: 'string', nullable: true, description: 'Auth profile override: null=default, "inherit"=main session profile, or "provider:label"' },
49
+ auth_profile_fallback: { type: 'string', nullable: true, description: 'Optional fallback auth profile for a same-run retry after primary selection failure' },
48
50
  delivery_opt_out_reason: { type: 'string', nullable: true, maxLength: 256 },
49
51
  delete_after_run: { type: 'boolean', default: false },
50
52
  run_now: { type: 'boolean', default: false, note: 'create-time convenience flag' },
package/schema.sql CHANGED
@@ -1,4 +1,4 @@
1
- -- OpenClaw Scheduler Schema (current: v1.7.0, schema version: 23)
1
+ -- OpenClaw Scheduler Schema (current: v1.7.0, schema version: 24)
2
2
  -- Full standalone scheduler + message router
3
3
 
4
4
  -- ============================================================
@@ -23,6 +23,7 @@ CREATE TABLE IF NOT EXISTS jobs (
23
23
  payload_kind TEXT NOT NULL, -- 'systemEvent' | 'agentTurn' | 'shellCommand'
24
24
  payload_message TEXT NOT NULL,
25
25
  payload_model TEXT,
26
+ payload_model_fallback TEXT,
26
27
  payload_thinking TEXT,
27
28
  payload_timeout_seconds INTEGER DEFAULT 120,
28
29
  execution_intent TEXT NOT NULL DEFAULT 'execute', -- 'execute' | 'plan'
@@ -91,6 +92,9 @@ CREATE TABLE IF NOT EXISTS jobs (
91
92
  -- Auth profile override (v16)
92
93
  auth_profile TEXT DEFAULT NULL, -- null=default, 'inherit'=main session profile, or 'provider:label'
93
94
 
95
+ -- Fallback selection overrides (v24)
96
+ auth_profile_fallback TEXT DEFAULT NULL, -- optional fallback auth profile used after primary selection failure
97
+
94
98
  -- Delivery opt-out (v19)
95
99
  delivery_opt_out_reason TEXT DEFAULT NULL, -- set when delivery_mode='none' to explicitly skip delivery
96
100
 
@@ -478,3 +482,4 @@ INSERT OR IGNORE INTO schema_migrations (version) VALUES (20);
478
482
  INSERT OR IGNORE INTO schema_migrations (version) VALUES (21);
479
483
  INSERT OR IGNORE INTO schema_migrations (version) VALUES (22);
480
484
  INSERT OR IGNORE INTO schema_migrations (version) VALUES (23);
485
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES (24);
package/setup.mjs CHANGED
@@ -19,7 +19,7 @@ import os from 'os';
19
19
  import { execSync } from 'child_process';
20
20
 
21
21
  import { fileURLToPath } from 'url';
22
- import { ensureSchedulerDbParent, resolveSchedulerDbPath } from './paths.js';
22
+ import { ensureSchedulerDbParent, resolveSchedulerDbPath, resolveServiceWorkingDirectory } from './paths.js';
23
23
  import { createJob } from './jobs.js';
24
24
  import { initDb } from './db.js';
25
25
 
@@ -152,7 +152,8 @@ print();
152
152
  // --- Step 1: Paths ------------------------------------------------------------
153
153
 
154
154
  print('-- Step 1: Paths ---------------------------------------');
155
- const schedulerPath = __dirname;
155
+ const schedulerInstallRoot = __dirname;
156
+ const serviceWorkingDirectory = resolveServiceWorkingDirectory({ env: process.env, moduleDir: schedulerInstallRoot });
156
157
  const defaultWorkspace = path.join(os.homedir(), '.openclaw', 'workspace');
157
158
  const workspacePath = await ask('Workspace path', defaultWorkspace);
158
159
  const defaultGateway = 'http://127.0.0.1:18789';
@@ -162,10 +163,11 @@ const schedulerDbPath = resolveSchedulerDbPath({ env: process.env });
162
163
  if (schedulerDbPath !== ':memory:') ensureSchedulerDbParent(schedulerDbPath);
163
164
 
164
165
  print();
165
- print(` Scheduler: ${schedulerPath}`);
166
- print(` Workspace: ${workspacePath}`);
167
- print(` Gateway: ${gatewayUrl}`);
168
- print(` Deliver to: ${deliverTo || '(none -- skipping job creation)'}`);
166
+ print(` Scheduler install root: ${schedulerInstallRoot}`);
167
+ print(` Service working dir: ${serviceWorkingDirectory}`);
168
+ print(` Workspace: ${workspacePath}`);
169
+ print(` Gateway: ${gatewayUrl}`);
170
+ print(` Deliver to: ${deliverTo || '(none -- skipping job creation)'}`);
169
171
  print();
170
172
 
171
173
  // --- Preflight: npm install behavior -----------------------------------------
@@ -193,9 +195,9 @@ print();
193
195
 
194
196
  print('-- Step 2: Database migrations -------------------------');
195
197
  try {
196
- const { setDbPath } = await import(path.join(schedulerPath, 'db.js'));
198
+ const { setDbPath } = await import(path.join(schedulerInstallRoot, 'db.js'));
197
199
  setDbPath(schedulerDbPath);
198
- const migrate = (await import(path.join(schedulerPath, 'migrate-consolidate.js'))).default;
200
+ const migrate = (await import(path.join(schedulerInstallRoot, 'migrate-consolidate.js'))).default;
199
201
  const ran = migrate();
200
202
  if (ran) {
201
203
  ok(`Migrations applied -> ${schedulerDbPath}`);
@@ -213,9 +215,9 @@ print();
213
215
  print('-- Step 3: Agent memory files --------------------------');
214
216
 
215
217
  const memoryMd = path.join(workspacePath, 'MEMORY.md');
216
- const memoryEntry = `- **Scheduler Queue Pattern:** Use \`node ${schedulerPath}/cli.js msg send <from> <to> "body"\` for signal-only queue entries.
217
- Inbox Consumer (\`${schedulerPath}/scripts/inbox-consumer.mjs\`) drains pending queue messages to Telegram.
218
- Stuck Run Detector (\`${schedulerPath}/scripts/stuck-run-detector.mjs\`) alerts on stale \`running\` runs.`;
218
+ const memoryEntry = `- **Scheduler Queue Pattern:** Use \`node ${schedulerInstallRoot}/cli.js msg send <from> <to> "body"\` for signal-only queue entries.
219
+ Inbox Consumer (\`${schedulerInstallRoot}/scripts/inbox-consumer.mjs\`) drains pending queue messages to Telegram.
220
+ Stuck Run Detector (\`${schedulerInstallRoot}/scripts/stuck-run-detector.mjs\`) alerts on stale \`running\` runs.`;
219
221
 
220
222
  const memResult = appendIfMissing(memoryMd, 'Scheduler Queue Pattern', memoryEntry);
221
223
  if (memResult === true) ok('Appended scheduler queue entry -> MEMORY.md');
@@ -228,10 +230,10 @@ const indexSection = `### Scheduler & Dispatch
228
230
 
229
231
  | File | Covers | Load |
230
232
  |------|--------|------|
231
- | \`${schedulerPath}/\` | Standalone SQLite scheduler. CLI: \`node cli.js\`. launchd service: \`ai.openclaw.scheduler\`. | Any scheduler/cron work |
232
- | \`${schedulerPath}/cli.js\` | Queue + run operations: \`msg send\`, \`msg inbox\`, \`runs running\`, \`runs stale\`. | Day-to-day scheduler operations |
233
- | \`${schedulerPath}/scripts/inbox-consumer.mjs\` | Drains queue messages for one agent and delivers to Telegram. | Queue/inbox consumption |
234
- | \`${schedulerPath}/scripts/stuck-run-detector.mjs\` | Detects stale \`running\` runs and exits non-zero for alerts. | Run health monitoring |`;
233
+ | \`${schedulerInstallRoot}/\` | Standalone SQLite scheduler. CLI: \`node cli.js\`. launchd service: \`ai.openclaw.scheduler\`. | Any scheduler/cron work |
234
+ | \`${schedulerInstallRoot}/cli.js\` | Queue + run operations: \`msg send\`, \`msg inbox\`, \`runs running\`, \`runs stale\`. | Day-to-day scheduler operations |
235
+ | \`${schedulerInstallRoot}/scripts/inbox-consumer.mjs\` | Drains queue messages for one agent and delivers to Telegram. | Queue/inbox consumption |
236
+ | \`${schedulerInstallRoot}/scripts/stuck-run-detector.mjs\` | Detects stale \`running\` runs and exits non-zero for alerts. | Run health monitoring |`;
235
237
 
236
238
  // Try inserting before a common section header, fall back to append.
237
239
  // NOTE: the link emoji anchors must match the actual markdown heading in
@@ -278,7 +280,7 @@ if (!deliverTo) {
278
280
  const existingNames = listJobs().map(r => r.name);
279
281
 
280
282
  // Inbox Consumer
281
- const icScript = path.join(schedulerPath, 'scripts', 'inbox-consumer.mjs');
283
+ const icScript = path.join(schedulerInstallRoot, 'scripts', 'inbox-consumer.mjs');
282
284
  const icName = 'Inbox Consumer';
283
285
  if (existingNames.includes(icName)) {
284
286
  skip(`"${icName}" job already exists`);
@@ -304,7 +306,7 @@ if (!deliverTo) {
304
306
 
305
307
  // Stuck Run Detector
306
308
  const srdName = 'Stuck Run Detector';
307
- const srdScript = path.join(schedulerPath, 'scripts', 'stuck-run-detector.mjs');
309
+ const srdScript = path.join(schedulerInstallRoot, 'scripts', 'stuck-run-detector.mjs');
308
310
  const srdCmd = `node ${srdScript} --threshold-min 45`; // coding tasks regularly take 30m+
309
311
  if (existingNames.includes(srdName)) {
310
312
  skip(`"${srdName}" job already exists`);
@@ -338,7 +340,7 @@ print();
338
340
 
339
341
  const platform = process.platform;
340
342
  const nodePath = process.execPath;
341
- const indexPath = path.join(schedulerPath, 'dispatcher.js');
343
+ const indexPath = path.join(schedulerInstallRoot, 'dispatcher.js');
342
344
  const logPath = platform === 'win32'
343
345
  ? path.join(os.tmpdir(), 'openclaw-scheduler.log')
344
346
  : '/tmp/openclaw-scheduler.log';
@@ -457,7 +459,7 @@ if (platform === 'darwin') {
457
459
  <string>${xmlEscape(indexPath)}</string>
458
460
  </array>
459
461
  ${userXml} <key>WorkingDirectory</key>
460
- <string>${xmlEscape(schedulerPath)}</string>
462
+ <string>${xmlEscape(serviceWorkingDirectory)}</string>
461
463
  <key>EnvironmentVariables</key>
462
464
  <dict>
463
465
  <key>HOME</key>
@@ -565,7 +567,7 @@ After=network.target
565
567
 
566
568
  [Service]
567
569
  Type=simple
568
- WorkingDirectory=${schedulerPath}
570
+ WorkingDirectory=${serviceWorkingDirectory}
569
571
  ExecStart=${nodePath} --no-warnings ${indexPath}
570
572
  Environment=OPENCLAW_GATEWAY_URL=${gatewayUrl}${gatewayToken ? `\nEnvironment="OPENCLAW_GATEWAY_TOKEN=${gatewayToken.replace(/"/g, '\\"')}"` : ''}
571
573
  Environment=SCHEDULER_DB=${schedulerDbPath}
@@ -612,7 +614,7 @@ WantedBy=default.target
612
614
  if (install) {
613
615
  try {
614
616
  execSync(
615
- `pm2 start "${indexPath}" --name "${pm2Name}" --cwd "${schedulerPath}" ` +
617
+ `pm2 start "${indexPath}" --name "${pm2Name}" --cwd "${serviceWorkingDirectory}" ` +
616
618
  `--log "${logPath}"`,
617
619
  {
618
620
  stdio: 'inherit',
@@ -652,7 +654,7 @@ WantedBy=default.target
652
654
  print(' Setup steps:');
653
655
  print(' 1. Install WSL2: wsl --install (in PowerShell as Admin)');
654
656
  print(' 2. Open your WSL terminal and run this wizard again from there:');
655
- print(` cd ${schedulerPath.replace(/\\/g, '/')}`);
657
+ print(` cd ${schedulerInstallRoot.replace(/\\/g, '/')}`);
656
658
  print(' node setup.mjs');
657
659
  print();
658
660
  print(' WSL2 with systemd enabled gives the best experience (auto-start on login).');