openclaw-scheduler 0.2.8 → 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.
package/index.d.ts CHANGED
@@ -33,6 +33,7 @@ export interface JobSpec {
33
33
  payload_kind?: 'systemEvent' | 'agentTurn' | 'shellCommand';
34
34
  payload_message: string;
35
35
  payload_model?: string | null;
36
+ payload_model_fallback?: string | null;
36
37
  payload_thinking?: string | null;
37
38
  payload_timeout_seconds?: number;
38
39
  payload_scope?: 'own' | 'global';
@@ -87,6 +88,7 @@ export interface JobSpec {
87
88
 
88
89
  // Auth profile override
89
90
  auth_profile?: string | null;
91
+ auth_profile_fallback?: string | null;
90
92
 
91
93
  // Delivery opt-out
92
94
  delivery_opt_out_reason?: string | null;
@@ -150,6 +152,8 @@ export interface JobRecord extends JobSpec {
150
152
  schedule_cron: string | null;
151
153
  schedule_at: string | null;
152
154
  schedule_tz: string;
155
+ payload_model_fallback?: string | null;
156
+ auth_profile_fallback?: string | null;
153
157
  payload_kind: 'systemEvent' | 'agentTurn' | 'shellCommand';
154
158
  payload_message: string;
155
159
  ttl_hours: number | null;
@@ -455,6 +459,7 @@ export interface AgentTurnWithTimeoutOpts {
455
459
  sessionKey?: string;
456
460
  model?: string;
457
461
  authProfile?: string | null;
462
+ sessionKinds?: string[];
458
463
  idleTimeoutMs?: number;
459
464
  pollIntervalMs?: number;
460
465
  absoluteTimeoutMs?: number;
package/jobs.js CHANGED
@@ -35,11 +35,11 @@ function sqliteNow(offsetMs = 0) {
35
35
 
36
36
  const PATCHABLE_COLUMNS = new Set([
37
37
  'enabled', 'name', 'schedule_cron', 'schedule_tz', 'schedule_at', 'schedule_kind',
38
- 'next_run_at', 'last_run_at', 'last_status', 'payload_message', 'payload_model',
38
+ 'next_run_at', 'last_run_at', 'last_status', 'payload_message', 'payload_model', 'payload_model_fallback',
39
39
  'payload_thinking', 'payload_timeout_seconds', 'session_target', 'run_timeout_ms',
40
40
  'max_retries', 'consecutive_errors',
41
41
  'delivery_mode', 'delivery_channel', 'delivery_to', 'delivery_opt_out_reason',
42
- 'delete_after_run', 'ttl_hours', 'auth_profile', 'origin',
42
+ 'delete_after_run', 'ttl_hours', 'auth_profile', 'auth_profile_fallback', 'origin',
43
43
  'output_excerpt_limit_bytes', 'output_summary_limit_bytes',
44
44
  'watchdog_check_cmd', 'watchdog_timeout_min', 'watchdog_started_at',
45
45
  'watchdog_target_label', 'watchdog_alert_channel', 'watchdog_alert_target',
@@ -192,9 +192,11 @@ export function validateJobSpec(opts, currentJob = null, mode = 'create') {
192
192
  'resource_pool',
193
193
  'preferred_session_key',
194
194
  'payload_model',
195
+ 'payload_model_fallback',
195
196
  'payload_thinking',
196
197
  'trigger_condition',
197
198
  'auth_profile',
199
+ 'auth_profile_fallback',
198
200
  'delivery_opt_out_reason',
199
201
  'origin',
200
202
  // v0.2 nullable strings
@@ -397,6 +399,9 @@ export function validateJobSpec(opts, currentJob = null, mode = 'create') {
397
399
  if (mode === 'create' || 'payload_model' in normalized) {
398
400
  assertSafeString('payload_model', merged.payload_model, { allowEmpty: false, maxLength: 256 });
399
401
  }
402
+ if (mode === 'create' || 'payload_model_fallback' in normalized) {
403
+ assertSafeString('payload_model_fallback', merged.payload_model_fallback, { allowEmpty: false, maxLength: 256 });
404
+ }
400
405
  if (mode === 'create' || 'payload_thinking' in normalized) {
401
406
  assertSafeString('payload_thinking', merged.payload_thinking, { allowEmpty: false, maxLength: 64 });
402
407
  }
@@ -408,6 +413,14 @@ export function validateJobSpec(opts, currentJob = null, mode = 'create') {
408
413
  assertSafeString('auth_profile', merged.auth_profile, { allowEmpty: false, maxLength: 256 });
409
414
  }
410
415
  }
416
+ if (mode === 'create' || 'auth_profile_fallback' in normalized) {
417
+ if (merged.auth_profile_fallback != null) {
418
+ if (typeof merged.auth_profile_fallback !== 'string') {
419
+ throw new Error('auth_profile_fallback must be a string or null');
420
+ }
421
+ assertSafeString('auth_profile_fallback', merged.auth_profile_fallback, { allowEmpty: false, maxLength: 256 });
422
+ }
423
+ }
411
424
 
412
425
  // Origin tracking (v20): required on creation for root (non-child) jobs.
413
426
  // Format convention: "<channel>:<id>" e.g. "telegram:<your-user-id>", "telegram:<your-group-id>", or "system" for automated jobs.
@@ -651,7 +664,7 @@ export function createJob(opts) {
651
664
  INSERT INTO jobs (
652
665
  id, name, enabled, schedule_kind, schedule_at, schedule_cron, schedule_tz,
653
666
  session_target, agent_id, payload_kind, payload_message,
654
- payload_model, payload_thinking, payload_timeout_seconds,
667
+ payload_model, payload_model_fallback, payload_thinking, payload_timeout_seconds,
655
668
  execution_intent, execution_read_only,
656
669
  overlap_policy, run_timeout_ms, max_queued_dispatches, max_pending_approvals, max_trigger_fanout,
657
670
  delivery_mode, delivery_channel, delivery_to,
@@ -668,7 +681,7 @@ export function createJob(opts) {
668
681
  watchdog_timeout_min, watchdog_alert_channel, watchdog_alert_target,
669
682
  watchdog_self_destruct, watchdog_started_at,
670
683
  ttl_hours,
671
- auth_profile,
684
+ auth_profile, auth_profile_fallback,
672
685
  delivery_opt_out_reason,
673
686
  origin,
674
687
  identity_principal, identity_run_as, identity_attestation, identity_ref,
@@ -684,7 +697,7 @@ export function createJob(opts) {
684
697
  ) VALUES (
685
698
  ?, ?, ?, ?, ?, ?, ?,
686
699
  ?, ?, ?, ?,
687
- ?, ?, ?,
700
+ ?, ?, ?, ?,
688
701
  ?, ?,
689
702
  ?, ?, ?, ?, ?,
690
703
  ?, ?, ?,
@@ -698,7 +711,7 @@ export function createJob(opts) {
698
711
  ?, ?, ?,
699
712
  ?, ?, ?,
700
713
  ?, ?,
701
- ?,
714
+ ?, ?,
702
715
  ?,
703
716
  ?,
704
717
  ?,
@@ -728,6 +741,7 @@ export function createJob(opts) {
728
741
  finalKind,
729
742
  normalized.payload_message,
730
743
  normalized.payload_model || null,
744
+ normalized.payload_model_fallback || null,
731
745
  normalized.payload_thinking || null,
732
746
  normalized.payload_timeout_seconds ?? 120,
733
747
  normalized.execution_intent || 'execute',
@@ -771,6 +785,7 @@ export function createJob(opts) {
771
785
  normalized.watchdog_started_at || null,
772
786
  normalized.ttl_hours || null,
773
787
  normalized.auth_profile || null,
788
+ normalized.auth_profile_fallback || null,
774
789
  normalized.delivery_opt_out_reason || null,
775
790
  normalized.origin || null,
776
791
  normalized.identity_principal || null,
@@ -830,7 +845,7 @@ export function updateJob(id, patch) {
830
845
  const allowed = [
831
846
  'name', 'enabled', 'schedule_kind', 'schedule_at', 'schedule_cron', 'schedule_tz',
832
847
  'session_target', 'agent_id', 'payload_kind', 'payload_message',
833
- 'payload_model', 'payload_thinking', 'payload_timeout_seconds',
848
+ 'payload_model', 'payload_model_fallback', 'payload_thinking', 'payload_timeout_seconds',
834
849
  'execution_intent', 'execution_read_only',
835
850
  'overlap_policy', 'run_timeout_ms', 'max_queued_dispatches', 'max_pending_approvals', 'max_trigger_fanout',
836
851
  'delivery_mode', 'delivery_channel', 'delivery_to',
@@ -846,7 +861,7 @@ export function updateJob(id, patch) {
846
861
  'watchdog_timeout_min', 'watchdog_alert_channel', 'watchdog_alert_target',
847
862
  'watchdog_self_destruct', 'watchdog_started_at',
848
863
  'ttl_hours',
849
- 'auth_profile',
864
+ 'auth_profile', 'auth_profile_fallback',
850
865
  'delivery_opt_out_reason',
851
866
  'origin',
852
867
  // v0.2 fields
@@ -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.8",
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",
@@ -44,6 +44,7 @@
44
44
  "dispatch/index.mjs",
45
45
  "dispatch/liveness.mjs",
46
46
  "dispatch/message-input.mjs",
47
+ "dispatch/paths.mjs",
47
48
  "dispatch/README.md",
48
49
  "dispatch/watcher.mjs",
49
50
  "scripts/dispatch-cli-utils.mjs",
@@ -122,7 +123,7 @@
122
123
  },
123
124
  "homepage": "https://github.com/amittell/openclaw-scheduler#readme",
124
125
  "dependencies": {
125
- "better-sqlite3": "^11.10.0",
126
+ "better-sqlite3": "^12.10.0",
126
127
  "croner": "^10.0.1"
127
128
  },
128
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).');